@@ -5,207 +5,146 @@ import { Log } from "../util/log"
55import { createOpencodeClient } from "@opencode-ai/sdk"
66import { Server } from "../server/server"
77import { BunProc } from "../bun"
8+ import { Instance } from "../project/instance"
89import { Flag } from "../flag/flag"
910import { CodexAuthPlugin } from "./codex"
1011import { Session } from "../session"
1112import { NamedError } from "@opencode-ai/util/error"
1213import { CopilotAuthPlugin } from "./copilot"
13- import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
14- import { PoeAuthPlugin } from "opencode-poe-auth"
15- import { Effect , Layer , ServiceMap } from "effect"
16- import { InstanceState } from "@/effect/instance-state"
17- import { makeRunPromise } from "@/effect/run-service"
14+ import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-auth"
1815
1916export namespace Plugin {
2017 const log = Log . create ( { service : "plugin" } )
2118
22- type State = {
23- hooks : Hooks [ ]
24- }
25-
26- // Hook names that follow the (input, output) => Promise<void> trigger pattern
27- type TriggerName = {
28- [ K in keyof Hooks ] -?: NonNullable < Hooks [ K ] > extends ( input : any , output : any ) => Promise < void > ? K : never
29- } [ keyof Hooks ]
30-
31- export interface Interface {
32- readonly trigger : <
33- Name extends TriggerName ,
34- Input = Parameters < Required < Hooks > [ Name ] > [ 0 ] ,
35- Output = Parameters < Required < Hooks > [ Name ] > [ 1 ] ,
36- > (
37- name : Name ,
38- input : Input ,
39- output : Output ,
40- ) => Effect . Effect < Output >
41- readonly list : ( ) => Effect . Effect < Hooks [ ] >
42- readonly init : ( ) => Effect . Effect < void >
43- }
44-
45- export class Service extends ServiceMap . Service < Service , Interface > ( ) ( "@opencode/Plugin" ) { }
19+ const BUILTIN = [ "[email protected] " ] 4620
4721 // Built-in plugins that are directly imported (not installed from npm)
48- const INTERNAL_PLUGINS : PluginInstance [ ] = [ CodexAuthPlugin , CopilotAuthPlugin , GitlabAuthPlugin , PoeAuthPlugin ]
49-
50- // Old npm package names for plugins that are now built-in — skip if users still have them in config
51- const DEPRECATED_PLUGIN_PACKAGES = [ "opencode-openai-codex-auth" , "opencode-copilot-auth" ]
52-
53- export const layer = Layer . effect (
54- Service ,
55- Effect . gen ( function * ( ) {
56- const cache = yield * InstanceState . make < State > (
57- Effect . fn ( "Plugin.state" ) ( function * ( ctx ) {
58- const hooks : Hooks [ ] = [ ]
59-
60- yield * Effect . promise ( async ( ) => {
61- const client = createOpencodeClient ( {
62- baseUrl : "http://localhost:4096" ,
63- directory : ctx . directory ,
64- headers : Flag . OPENCODE_SERVER_PASSWORD
65- ? {
66- Authorization : `Basic ${ Buffer . from ( `${ Flag . OPENCODE_SERVER_USERNAME ?? "opencode" } :${ Flag . OPENCODE_SERVER_PASSWORD } ` ) . toString ( "base64" ) } ` ,
67- }
68- : undefined ,
69- fetch : async ( ...args ) => Server . Default ( ) . fetch ( ...args ) ,
70- } )
71- const cfg = await Config . get ( )
72- const input : PluginInput = {
73- client,
74- project : ctx . project ,
75- worktree : ctx . worktree ,
76- directory : ctx . directory ,
77- get serverUrl ( ) : URL {
78- return Server . url ?? new URL ( "http://localhost:4096" )
79- } ,
80- $ : Bun . $ ,
81- }
82-
83- for ( const plugin of INTERNAL_PLUGINS ) {
84- log . info ( "loading internal plugin" , { name : plugin . name } )
85- const init = await plugin ( input ) . catch ( ( err ) => {
86- log . error ( "failed to load internal plugin" , { name : plugin . name , error : err } )
87- } )
88- if ( init ) hooks . push ( init )
89- }
90-
91- let plugins = cfg . plugin ?? [ ]
92- if ( plugins . length ) await Config . waitForDependencies ( )
93-
94- for ( let plugin of plugins ) {
95- if ( DEPRECATED_PLUGIN_PACKAGES . some ( ( pkg ) => plugin . includes ( pkg ) ) ) continue
96- log . info ( "loading plugin" , { path : plugin } )
97- if ( ! plugin . startsWith ( "file://" ) ) {
98- const idx = plugin . lastIndexOf ( "@" )
99- const pkg = idx > 0 ? plugin . substring ( 0 , idx ) : plugin
100- const version = idx > 0 ? plugin . substring ( idx + 1 ) : "latest"
101- plugin = await BunProc . install ( pkg , version ) . catch ( ( err ) => {
102- const cause = err instanceof Error ? err . cause : err
103- const detail = cause instanceof Error ? cause . message : String ( cause ?? err )
104- log . error ( "failed to install plugin" , { pkg, version, error : detail } )
105- Bus . publish ( Session . Event . Error , {
106- error : new NamedError . Unknown ( {
107- message : `Failed to install plugin ${ pkg } @${ version } : ${ detail } ` ,
108- } ) . toObject ( ) ,
109- } )
110- return ""
111- } )
112- if ( ! plugin ) continue
113- }
114-
115- // Prevent duplicate initialization when plugins export the same function
116- // as both a named export and default export (e.g., `export const X` and `export default X`).
117- // Object.entries(mod) would return both entries pointing to the same function reference.
118- await import ( plugin )
119- . then ( async ( mod ) => {
120- const seen = new Set < PluginInstance > ( )
121- for ( const [ _name , fn ] of Object . entries < PluginInstance > ( mod ) ) {
122- if ( seen . has ( fn ) ) continue
123- seen . add ( fn )
124- hooks . push ( await fn ( input ) )
125- }
126- } )
127- . catch ( ( err ) => {
128- const message = err instanceof Error ? err . message : String ( err )
129- log . error ( "failed to load plugin" , { path : plugin , error : message } )
130- Bus . publish ( Session . Event . Error , {
131- error : new NamedError . Unknown ( {
132- message : `Failed to load plugin ${ plugin } : ${ message } ` ,
133- } ) . toObject ( ) ,
134- } )
135- } )
136- }
137-
138- // Notify plugins of current config
139- for ( const hook of hooks ) {
140- try {
141- await ( hook as any ) . config ?.( cfg )
142- } catch ( err ) {
143- log . error ( "plugin config hook failed" , { error : err } )
144- }
145- }
22+ const INTERNAL_PLUGINS : PluginInstance [ ] = [ CodexAuthPlugin , CopilotAuthPlugin , GitlabAuthPlugin ]
23+
24+ const state = Instance . state ( async ( ) => {
25+ const client = createOpencodeClient ( {
26+ baseUrl : "http://localhost:4096" ,
27+ directory : Instance . directory ,
28+ // @ts -ignore - fetch type incompatibility
29+ fetch : async ( ...args ) => Server . App ( ) . fetch ( ...args ) ,
30+ } )
31+ const config = await Config . get ( )
32+ const hooks : Hooks [ ] = [ ]
33+ const input : PluginInput = {
34+ client,
35+ project : Instance . project ,
36+ worktree : Instance . worktree ,
37+ directory : Instance . directory ,
38+ serverUrl : Server . url ( ) ,
39+ $ : Bun . $ ,
40+ }
41+
42+ for ( const plugin of INTERNAL_PLUGINS ) {
43+ log . info ( "loading internal plugin" , { name : plugin . name } )
44+ const init = await plugin ( input ) . catch ( ( err ) => {
45+ log . error ( "failed to load internal plugin" , { name : plugin . name , error : err } )
46+ } )
47+ if ( init ) hooks . push ( init )
48+ }
49+
50+ let plugins = config . plugin ?? [ ]
51+ if ( plugins . length ) await Config . waitForDependencies ( )
52+ if ( ! Flag . OPENCODE_DISABLE_DEFAULT_PLUGINS ) {
53+ plugins = [ ...BUILTIN , ...plugins ]
54+ }
55+
56+ for ( let plugin of plugins ) {
57+ // ignore old codex plugin since it is supported first party now
58+ if ( plugin . includes ( "opencode-openai-codex-auth" ) || plugin . includes ( "opencode-copilot-auth" ) ) continue
59+ log . info ( "loading plugin" , { path : plugin } )
60+ if ( ! plugin . startsWith ( "file://" ) ) {
61+ const lastAtIndex = plugin . lastIndexOf ( "@" )
62+ const pkg = lastAtIndex > 0 ? plugin . substring ( 0 , lastAtIndex ) : plugin
63+ const version = lastAtIndex > 0 ? plugin . substring ( lastAtIndex + 1 ) : "latest"
64+ plugin = await BunProc . install ( pkg , version ) . catch ( ( err ) => {
65+ const cause = err instanceof Error ? err . cause : err
66+ const detail = cause instanceof Error ? cause . message : String ( cause ?? err )
67+ log . error ( "failed to install plugin" , { pkg, version, error : detail } )
68+ Bus . publish ( Session . Event . Error , {
69+ error : new NamedError . Unknown ( {
70+ message : `Failed to install plugin ${ pkg } @${ version } : ${ detail } ` ,
71+ } ) . toObject ( ) ,
14672 } )
147-
148- // Subscribe to bus events, clean up when scope is closed
149- yield * Effect . acquireRelease (
150- Effect . sync ( ( ) =>
151- Bus . subscribeAll ( async ( input ) => {
152- for ( const hook of hooks ) {
153- hook [ "event" ] ?.( { event : input } )
154- }
155- } ) ,
156- ) ,
157- ( unsub ) => Effect . sync ( unsub ) ,
158- )
159-
160- return { hooks }
161- } ) ,
162- )
163-
164- const trigger = Effect . fn ( "Plugin.trigger" ) ( function * <
165- Name extends TriggerName ,
166- Input = Parameters < Required < Hooks > [ Name ] > [ 0 ] ,
167- Output = Parameters < Required < Hooks > [ Name ] > [ 1 ] ,
168- > ( name : Name , input : Input , output : Output ) {
169- if ( ! name ) return output
170- const state = yield * InstanceState . get ( cache )
171- yield * Effect . promise ( async ( ) => {
172- for ( const hook of state . hooks ) {
173- const fn = hook [ name ] as any
174- if ( ! fn ) continue
175- await fn ( input , output )
73+ return ""
74+ } )
75+ if ( ! plugin ) continue
76+ }
77+ // Prevent duplicate initialization when plugins export the same function
78+ // as both a named export and default export (e.g., `export const X` and `export default X`).
79+ // Object.entries(mod) would return both entries pointing to the same function reference.
80+ await import ( plugin )
81+ . then ( async ( mod ) => {
82+ const seen = new Set < PluginInstance > ( )
83+ for ( const [ _name , fn ] of Object . entries < PluginInstance > ( mod ) ) {
84+ if ( seen . has ( fn ) ) continue
85+ seen . add ( fn )
86+ hooks . push ( await fn ( input ) )
17687 }
17788 } )
178- return output
179- } )
180-
181- const list = Effect . fn ( "Plugin.list" ) ( function * ( ) {
182- const state = yield * InstanceState . get ( cache )
183- return state . hooks
184- } )
185-
186- const init = Effect . fn ( "Plugin.init" ) ( function * ( ) {
187- yield * InstanceState . get ( cache )
188- } )
189-
190- return Service . of ( { trigger, list, init } )
191- } ) ,
192- )
193-
194- const runPromise = makeRunPromise ( Service , layer )
89+ . catch ( ( err ) => {
90+ const message = err instanceof Error ? err . message : String ( err )
91+ log . error ( "failed to load plugin" , { path : plugin , error : message } )
92+ Bus . publish ( Session . Event . Error , {
93+ error : new NamedError . Unknown ( {
94+ message : `Failed to load plugin ${ plugin } : ${ message } ` ,
95+ } ) . toObject ( ) ,
96+ } )
97+ } )
98+ }
99+
100+ return {
101+ hooks,
102+ input,
103+ unsubscribe : undefined as ( ( ) => void ) | undefined ,
104+ }
105+ } ,
106+ async ( state ) => {
107+ state . unsubscribe ?.( )
108+ for ( const hook of state . hooks ) {
109+ await hook . dispose ?.( )
110+ }
111+ } )
195112
196113 export async function trigger <
197- Name extends TriggerName ,
114+ Name extends Exclude < keyof Required < Hooks > , "auth" | "event" | "tool" > ,
198115 Input = Parameters < Required < Hooks > [ Name ] > [ 0 ] ,
199116 Output = Parameters < Required < Hooks > [ Name ] > [ 1 ] ,
200117 > ( name : Name , input : Input , output : Output ) : Promise < Output > {
201- return runPromise ( ( svc ) => svc . trigger ( name , input , output ) )
118+ if ( ! name ) return output
119+ for ( const hook of await state ( ) . then ( ( x ) => x . hooks ) ) {
120+ const fn = hook [ name ]
121+ if ( ! fn ) continue
122+ // @ts -expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
123+ // give up.
124+ // try-counter: 2
125+ await fn ( input , output )
126+ }
127+ return output
202128 }
203129
204- export async function list ( ) : Promise < Hooks [ ] > {
205- return runPromise ( ( svc ) => svc . list ( ) )
130+ export async function list ( ) {
131+ return state ( ) . then ( ( x ) => x . hooks )
206132 }
207133
208134 export async function init ( ) {
209- return runPromise ( ( svc ) => svc . init ( ) )
135+ const s = await state ( )
136+ const config = await Config . get ( )
137+ for ( const hook of s . hooks ) {
138+ // @ts -expect-error this is because we haven't moved plugin to sdk v2
139+ await hook . config ?.( config )
140+ }
141+ s . unsubscribe = Bus . subscribeAll ( async ( input ) => {
142+ const hooks = await state ( ) . then ( ( x ) => x . hooks )
143+ for ( const hook of hooks ) {
144+ hook [ "event" ] ?.( {
145+ event : input ,
146+ } )
147+ }
148+ } )
210149 }
211150}
0 commit comments