@@ -6,7 +6,7 @@ import { loadSettings, repoUrl, settingsPath } from "../core/settings.js";
66import { pull , push , status } from "../core/sync.js" ;
77
88/**
9- * OpenCode plugin entry point.
9+ * OpenCode plugin entry point (dual V1 + V2) .
1010 *
1111 * The plugin is a convenience layer, never the only way in. It runs inside the
1212 * OpenCode process, which means it cannot help when the configuration it just
@@ -19,7 +19,15 @@ import { pull, push, status } from "../core/sync.js";
1919 * - an `opencode_sync` tool, so syncing can be asked for in plain language
2020 *
2121 * Everything is off unless the user configured a repository, and every failure
22- * is reported as a toast instead of taking OpenCode down with it.
22+ * is reported as a toast (V1) or a console line (V2) instead of taking
23+ * OpenCode down with it.
24+ *
25+ * V1 (OpenCode 1.x) calls the default export as a function and consumes the
26+ * returned `{ event, tool }` hook map. V2 (OpenCode 2.x) decodes the default
27+ * export for an `{ id, setup }` definition and calls `setup(ctx)`, which
28+ * registers the same capabilities through `ctx.tool.transform` and
29+ * `ctx.event.subscribe`. One default export carries both halves; the two
30+ * implementations are independent and share only the core helpers below.
2331 */
2432
2533type ToastVariant = "info" | "success" | "warning" | "error" ;
@@ -29,6 +37,25 @@ interface PluginContext {
2937 directory ?: string ;
3038}
3139
40+ /** Minimal structural slice of the V2 plugin context this file touches. */
41+ interface V2ToolEditor {
42+ add ( tool : {
43+ name : string ;
44+ description : string ;
45+ input : unknown ;
46+ execute : ( input : any ) => Promise < { content : string } > ;
47+ } ) : void ;
48+ }
49+
50+ interface V2Context {
51+ tool : {
52+ transform : ( callback : ( editor : V2ToolEditor ) => void ) => Promise < unknown > ;
53+ } ;
54+ event : {
55+ subscribe : ( options ?: { signal ?: AbortSignal } ) => AsyncIterable < { type : string } > ;
56+ } ;
57+ }
58+
3259async function toast ( client : any , message : string , variant : ToastVariant ) : Promise < void > {
3360 try {
3461 await client ?. tui ?. showToast ?.( { body : { message, variant } } ) ;
@@ -47,6 +74,17 @@ async function log(client: any, level: string, message: string, extra?: unknown)
4774 }
4875}
4976
77+ /** V2 equivalent of log/toast: the promise context has no app.log or TUI client. */
78+ function notice ( level : "info" | "warn" , message : string , extra ?: unknown ) : void {
79+ if ( level === "warn" ) {
80+ if ( extra !== undefined ) console . warn ( `[opencode-github-sync] ${ message } ` , extra ) ;
81+ else console . warn ( `[opencode-github-sync] ${ message } ` ) ;
82+ } else {
83+ if ( extra !== undefined ) console . log ( `[opencode-github-sync] ${ message } ` , extra ) ;
84+ else console . log ( `[opencode-github-sync] ${ message } ` ) ;
85+ }
86+ }
87+
5088function isConfigured ( ) : boolean {
5189 const roots = getRoots ( ) ;
5290 if ( ! fs . existsSync ( settingsPath ( roots . config ) ) ) return false ;
@@ -81,6 +119,29 @@ async function runPull(client: any, announce: boolean): Promise<void> {
81119 }
82120}
83121
122+ async function runPullV2 ( announce : boolean ) : Promise < void > {
123+ const roots = getRoots ( ) ;
124+ const reporter = new CollectingReporter ( ) ;
125+ try {
126+ const result = await withSyncLock ( roots . config , { waitMs : 30_000 } , ( ) =>
127+ pull ( { reporter, roots } ) ,
128+ ) ;
129+ notice ( "info" , `pull: ${ result . message } ` , { summary : result . summary } ) ;
130+ if ( result . changed ) {
131+ notice (
132+ "info" ,
133+ `Config updated from GitHub (${ result . files . length } file(s)). Restart OpenCode to apply.` ,
134+ ) ;
135+ } else if ( announce ) {
136+ notice ( "info" , "Config already up to date." ) ;
137+ }
138+ } catch ( error ) {
139+ const message = ( error as Error ) . message ;
140+ notice ( "warn" , `pull failed: ${ message } ` ) ;
141+ if ( announce ) notice ( "warn" , `Sync pull failed: ${ message } ` ) ;
142+ }
143+ }
144+
84145async function runPush ( client : any , announce : boolean ) : Promise < void > {
85146 const roots = getRoots ( ) ;
86147 const reporter = new CollectingReporter ( ) ;
@@ -99,6 +160,71 @@ async function runPush(client: any, announce: boolean): Promise<void> {
99160 }
100161}
101162
163+ async function runPushV2 ( announce : boolean ) : Promise < void > {
164+ const roots = getRoots ( ) ;
165+ const reporter = new CollectingReporter ( ) ;
166+ try {
167+ const result = await withSyncLock ( roots . config , { waitMs : 30_000 } , ( ) =>
168+ push ( { reporter, roots } ) ,
169+ ) ;
170+ notice ( "info" , `push: ${ result . message } ` ) ;
171+ if ( announce ) notice ( "info" , result . message ) ;
172+ } catch ( error ) {
173+ const message = ( error as Error ) . message ;
174+ notice ( "warn" , `push failed: ${ message } ` ) ;
175+ if ( announce ) notice ( "warn" , `Sync push failed: ${ message } ` ) ;
176+ }
177+ }
178+
179+ const SYNC_TOOL_DESCRIPTION =
180+ "Sync OpenCode configuration with the GitHub sync repository. " +
181+ "Use action 'push' to upload this machine's configuration, 'pull' to apply the shared " +
182+ "configuration, or 'status' to report what is out of sync." ;
183+
184+ const SYNC_TOOL_INPUT = {
185+ type : "object" ,
186+ properties : {
187+ action : {
188+ type : "string" ,
189+ enum : [ "push" , "pull" , "status" ] ,
190+ description : "Which sync operation to run." ,
191+ } ,
192+ } ,
193+ additionalProperties : false ,
194+ } as const ;
195+
196+ async function executeSyncAction ( args : { action ?: string } ) : Promise < string > {
197+ const roots = getRoots ( ) ;
198+ const action = args ?. action ?? "status" ;
199+ const reporter = new CollectingReporter ( ) ;
200+
201+ if ( action === "status" ) {
202+ const state = status ( { roots } ) ;
203+ return JSON . stringify ( state , null , 2 ) ;
204+ }
205+
206+ const result = await withSyncLock ( roots . config , { waitMs : 60_000 } , ( ) =>
207+ action === "push" ? push ( { reporter, roots } ) : pull ( { reporter, roots } ) ,
208+ ) ;
209+
210+ const lines = [ result . message ] ;
211+ if ( result . files . length > 0 ) {
212+ lines . push (
213+ `Files: ${ result . files
214+ . slice ( 0 , 20 )
215+ . map ( ( f ) => `${ f . kind [ 0 ] } ${ f . path } ` )
216+ . join ( ", " ) } `,
217+ ) ;
218+ }
219+ if ( result . restartRequired && result . changed ) {
220+ lines . push ( "Restart OpenCode for the new configuration to take effect." ) ;
221+ }
222+ if ( reporter . lines . length > 0 ) {
223+ lines . push ( ...reporter . lines . filter ( ( l ) => l . level === "warn" ) . map ( ( l ) => `! ${ l . message } ` ) ) ;
224+ }
225+ return lines . join ( "\n" ) ;
226+ }
227+
102228export const OpencodeGithubSync = async ( ctx : PluginContext ) => {
103229 const client = ctx ?. client ;
104230 const roots = getRoots ( ) ;
@@ -129,10 +255,7 @@ export const OpencodeGithubSync = async (ctx: PluginContext) => {
129255
130256 tool : {
131257 opencode_sync : {
132- description :
133- "Sync OpenCode configuration with the GitHub sync repository. " +
134- "Use action 'push' to upload this machine's configuration, 'pull' to apply the shared " +
135- "configuration, or 'status' to report what is out of sync." ,
258+ description : SYNC_TOOL_DESCRIPTION ,
136259 args : {
137260 action : {
138261 type : "string" ,
@@ -141,40 +264,67 @@ export const OpencodeGithubSync = async (ctx: PluginContext) => {
141264 } ,
142265 } ,
143266 async execute ( args : { action ?: string } ) {
144- const action = args ?. action ?? "status" ;
145- const reporter = new CollectingReporter ( ) ;
146-
147- if ( action === "status" ) {
148- const state = status ( { roots } ) ;
149- return JSON . stringify ( state , null , 2 ) ;
150- }
151-
152- const result = await withSyncLock ( roots . config , { waitMs : 60_000 } , ( ) =>
153- action === "push" ? push ( { reporter, roots } ) : pull ( { reporter, roots } ) ,
154- ) ;
155-
156- const lines = [ result . message ] ;
157- if ( result . files . length > 0 ) {
158- lines . push (
159- `Files: ${ result . files
160- . slice ( 0 , 20 )
161- . map ( ( f ) => `${ f . kind [ 0 ] } ${ f . path } ` )
162- . join ( ", " ) } `,
163- ) ;
164- }
165- if ( result . restartRequired && result . changed ) {
166- lines . push ( "Restart OpenCode for the new configuration to take effect." ) ;
167- }
168- if ( reporter . lines . length > 0 ) {
169- lines . push (
170- ...reporter . lines . filter ( ( l ) => l . level === "warn" ) . map ( ( l ) => `! ${ l . message } ` ) ,
171- ) ;
172- }
173- return lines . join ( "\n" ) ;
267+ return executeSyncAction ( args ) ;
174268 } ,
175269 } ,
176270 } ,
177271 } ;
178272} ;
179273
180- export default OpencodeGithubSync ;
274+ async function setup ( ctx : V2Context ) : Promise < ( ) => void > {
275+ const controller = new AbortController ( ) ;
276+ const roots = getRoots ( ) ;
277+
278+ if ( ! isConfigured ( ) ) {
279+ notice (
280+ "info" ,
281+ "opencode-github-sync is installed but no repository is configured. Run `opencode-sync init`." ,
282+ ) ;
283+ return ( ) => controller . abort ( ) ;
284+ }
285+
286+ const settings = loadSettings ( roots . config ) ;
287+
288+ if ( settings . autoPullOnStartup ) {
289+ // Deliberately not awaited: OpenCode should finish starting even when the
290+ // network is slow or GitHub is unreachable.
291+ void runPullV2 ( false ) ;
292+ }
293+
294+ await ctx . tool . transform ( ( editor ) => {
295+ editor . add ( {
296+ name : "opencode_sync" ,
297+ description : SYNC_TOOL_DESCRIPTION ,
298+ input : SYNC_TOOL_INPUT ,
299+ async execute ( input ) {
300+ return { content : await executeSyncAction ( input as { action ?: string } ) } ;
301+ } ,
302+ } ) ;
303+ } ) ;
304+
305+ if ( settings . autoPushOnIdle ) {
306+ void ( async ( ) => {
307+ try {
308+ for await ( const event of ctx . event . subscribe ( { signal : controller . signal } ) ) {
309+ if ( event . type === "session.idle" ) {
310+ void runPushV2 ( false ) ;
311+ }
312+ }
313+ } catch {
314+ // Aborted on unload — nothing to report.
315+ }
316+ } ) ( ) ;
317+ }
318+
319+ return ( ) => controller . abort ( ) ;
320+ }
321+
322+ const V2Plugin = {
323+ id : "opencode-github-sync" ,
324+ setup,
325+ } ;
326+
327+ export default {
328+ ...V2Plugin ,
329+ server : OpencodeGithubSync ,
330+ } ;
0 commit comments