2525
2626import { tool } from "@opencode-ai/plugin/tool" ;
2727import { promises as fsPromises } from "node:fs" ;
28- import { dirname } from "node:path" ;
28+ import { dirname , join } from "node:path" ;
2929import type { Plugin , PluginInput } from "@opencode-ai/plugin" ;
3030import type { Auth } from "@opencode-ai/sdk" ;
3131import {
@@ -3405,9 +3405,55 @@ while (attempted.size < Math.max(1, accountCount)) {
34053405 }
34063406 } ;
34073407
3408+ const SYNC_PRUNE_BACKUP_PREFIX = "codex-sync-prune-backup" ;
3409+ const SYNC_PRUNE_BACKUP_RETAIN_COUNT = 2 ;
3410+
3411+ const pruneOldSyncPruneBackups = async (
3412+ backupDir : string ,
3413+ keepPaths : string [ ] = [ ] ,
3414+ ) : Promise < void > => {
3415+ const entries = await fsPromises . readdir ( backupDir , { withFileTypes : true } ) . catch ( ( error ) => {
3416+ const code = ( error as NodeJS . ErrnoException ) . code ;
3417+ if ( code === "ENOENT" ) {
3418+ return null ;
3419+ }
3420+ throw error ;
3421+ } ) ;
3422+ if ( ! entries ) return ;
3423+
3424+ const keepSet = new Set ( keepPaths ) ;
3425+ const staleBackupPaths = entries
3426+ . filter (
3427+ ( entry ) =>
3428+ entry . isFile ( ) &&
3429+ entry . name . startsWith ( `${ SYNC_PRUNE_BACKUP_PREFIX } -` ) &&
3430+ entry . name . endsWith ( ".json" ) ,
3431+ )
3432+ . map ( ( entry ) => join ( backupDir , entry . name ) )
3433+ . filter ( ( candidatePath ) => ! keepSet . has ( candidatePath ) )
3434+ . sort ( ( left , right ) => right . localeCompare ( left ) )
3435+ . slice ( SYNC_PRUNE_BACKUP_RETAIN_COUNT ) ;
3436+
3437+ for ( const staleBackupPath of staleBackupPaths ) {
3438+ try {
3439+ await fsPromises . unlink ( staleBackupPath ) ;
3440+ } catch ( error ) {
3441+ const code = ( error as NodeJS . ErrnoException ) . code ;
3442+ if ( code === "ENOENT" ) {
3443+ continue ;
3444+ }
3445+ const message = error instanceof Error ? error . message : String ( error ) ;
3446+ logWarn (
3447+ `[${ PLUGIN_NAME } ] Failed to prune stale sync prune backup ${ staleBackupPath } : ${ message } ` ,
3448+ ) ;
3449+ }
3450+ }
3451+ } ;
3452+
34083453 const createSyncPruneBackup = async ( ) : Promise < {
34093454 backupPath : string ;
34103455 restore : ( ) => Promise < void > ;
3456+ cleanup : ( ) => Promise < void > ;
34113457 } > => {
34123458 const { accounts : loadedAccountsStorage , flagged : currentFlaggedStorage } =
34133459 await loadAccountAndFlaggedStorageSnapshot ( ) ;
@@ -3419,8 +3465,9 @@ while (attempted.size < Math.max(1, accountCount)) {
34193465 activeIndex : 0 ,
34203466 activeIndexByFamily : { } ,
34213467 } satisfies AccountStorageV3 ) ;
3422- const backupPath = createTimestampedBackupPath ( "codex-sync-prune-backup" ) ;
3423- await fsPromises . mkdir ( dirname ( backupPath ) , { recursive : true } ) ;
3468+ const backupPath = createTimestampedBackupPath ( SYNC_PRUNE_BACKUP_PREFIX ) ;
3469+ const backupDir = dirname ( backupPath ) ;
3470+ await fsPromises . mkdir ( backupDir , { recursive : true } ) ;
34243471 const backupPayload = createSyncPruneBackupPayload (
34253472 currentAccountsStorage ,
34263473 currentFlaggedStorage ,
@@ -3434,6 +3481,13 @@ while (attempted.size < Math.max(1, accountCount)) {
34343481 mode : 0o600 ,
34353482 } ) ;
34363483 await fsPromises . rename ( tempBackupPath , backupPath ) ;
3484+ await pruneOldSyncPruneBackups ( backupDir , [ backupPath ] ) . catch ( ( cleanupError ) => {
3485+ const cleanupMessage =
3486+ cleanupError instanceof Error ? cleanupError . message : String ( cleanupError ) ;
3487+ logWarn (
3488+ `[${ PLUGIN_NAME } ] Failed to prune old sync prune backups in ${ backupDir } : ${ cleanupMessage } ` ,
3489+ ) ;
3490+ } ) ;
34373491 } catch ( error ) {
34383492 try {
34393493 await fsPromises . unlink ( tempBackupPath ) ;
@@ -3484,6 +3538,17 @@ while (attempted.size < Math.max(1, accountCount)) {
34843538 } ) ;
34853539 invalidateAccountManagerCache ( ) ;
34863540 } ,
3541+ cleanup : async ( ) => {
3542+ try {
3543+ await fsPromises . unlink ( backupPath ) ;
3544+ } catch ( error ) {
3545+ const code = ( error as NodeJS . ErrnoException ) . code ;
3546+ if ( code !== "ENOENT" ) {
3547+ throw error ;
3548+ }
3549+ }
3550+ await pruneOldSyncPruneBackups ( backupDir ) ;
3551+ } ,
34873552 } ;
34883553 } ;
34893554
@@ -3665,12 +3730,35 @@ while (attempted.size < Math.max(1, accountCount)) {
36653730 return ;
36663731 }
36673732
3668- let pruneBackup : { backupPath : string ; restore : ( ) => Promise < void > } | null = null ;
3733+ let pruneBackup : {
3734+ backupPath : string ;
3735+ restore : ( ) => Promise < void > ;
3736+ cleanup : ( ) => Promise < void > ;
3737+ } | null = null ;
36693738 const restorePruneBackup = async ( ) : Promise < void > => {
36703739 const currentBackup = pruneBackup ;
36713740 if ( ! currentBackup ) return ;
36723741 await currentBackup . restore ( ) ;
36733742 pruneBackup = null ;
3743+ await currentBackup . cleanup ( ) . catch ( ( cleanupError ) => {
3744+ const cleanupMessage =
3745+ cleanupError instanceof Error ? cleanupError . message : String ( cleanupError ) ;
3746+ logWarn (
3747+ `[${ PLUGIN_NAME } ] Failed to delete sync prune backup ${ currentBackup . backupPath } : ${ cleanupMessage } ` ,
3748+ ) ;
3749+ } ) ;
3750+ } ;
3751+ const cleanupPruneBackup = async ( ) : Promise < void > => {
3752+ const currentBackup = pruneBackup ;
3753+ if ( ! currentBackup ) return ;
3754+ pruneBackup = null ;
3755+ await currentBackup . cleanup ( ) . catch ( ( cleanupError ) => {
3756+ const cleanupMessage =
3757+ cleanupError instanceof Error ? cleanupError . message : String ( cleanupError ) ;
3758+ logWarn (
3759+ `[${ PLUGIN_NAME } ] Failed to delete sync prune backup ${ currentBackup . backupPath } : ${ cleanupMessage } ` ,
3760+ ) ;
3761+ } ) ;
36743762 } ;
36753763
36763764 while ( true ) {
@@ -3695,7 +3783,7 @@ while (attempted.size < Math.max(1, accountCount)) {
36953783 }
36963784
36973785 const result = await syncFromCodexMultiAuth ( process . cwd ( ) , loadedSource ) ;
3698- pruneBackup = null ;
3786+ await cleanupPruneBackup ( ) ;
36993787 invalidateAccountManagerCache ( ) ;
37003788 const backupLabel =
37013789 result . backupStatus === "created"
0 commit comments