@@ -66,6 +66,7 @@ import {
6666 getActionableNamedBackupRestores ,
6767 getRedactedFilesystemErrorLabel ,
6868 getNamedBackupsDirectoryPath ,
69+ listAccountSnapshots ,
6970 listNamedBackups ,
7071 listRotatingBackups ,
7172 NAMED_BACKUP_LIST_CONCURRENCY ,
@@ -89,6 +90,7 @@ import {
8990 getCodexCliConfigPath ,
9091 loadCodexCliState ,
9192} from "./codex-cli/state.js" ;
93+ import { getLatestCodexCliSyncRollbackPlan } from "./codex-cli/sync.js" ;
9294import { setCodexCliActiveSelection } from "./codex-cli/writer.js" ;
9395import { ANSI } from "./ui/ansi.js" ;
9496import { confirm } from "./ui/confirm.js" ;
@@ -3348,21 +3350,24 @@ async function runDoctor(args: string[]): Promise<number> {
33483350
33493351 setStoragePath ( null ) ;
33503352 const storagePath = getStoragePath ( ) ;
3353+ const walPath = `${ storagePath } .wal` ;
3354+ const storageExists = existsSync ( storagePath ) ;
3355+ const walExists = existsSync ( walPath ) ;
33513356 const checks : DoctorCheck [ ] = [ ] ;
33523357 const addCheck = ( check : DoctorCheck ) : void => {
33533358 checks . push ( check ) ;
33543359 } ;
33553360
33563361 addCheck ( {
33573362 key : "storage-file" ,
3358- severity : existsSync ( storagePath ) ? "ok" : "warn" ,
3359- message : existsSync ( storagePath )
3363+ severity : storageExists ? "ok" : "warn" ,
3364+ message : storageExists
33603365 ? "Account storage file found"
33613366 : "Account storage file does not exist yet (first login pending)" ,
33623367 details : storagePath ,
33633368 } ) ;
33643369
3365- if ( existsSync ( storagePath ) ) {
3370+ if ( storageExists ) {
33663371 try {
33673372 const stat = await fs . stat ( storagePath ) ;
33683373 addCheck ( {
@@ -3381,6 +3386,82 @@ async function runDoctor(args: string[]): Promise<number> {
33813386 }
33823387 }
33833388
3389+ addCheck ( {
3390+ key : "storage-journal" ,
3391+ severity : walExists ? "ok" : "warn" ,
3392+ message : walExists
3393+ ? "Write-ahead journal found"
3394+ : "Write-ahead journal missing; recovery will rely on backups" ,
3395+ details : walPath ,
3396+ } ) ;
3397+
3398+ const rotatingBackups = await listRotatingBackups ( ) ;
3399+ const validRotatingBackups = rotatingBackups . filter ( ( backup ) => backup . valid ) ;
3400+ const invalidRotatingBackups = rotatingBackups . filter (
3401+ ( backup ) => ! backup . valid ,
3402+ ) ;
3403+ addCheck ( {
3404+ key : "rotating-backups" ,
3405+ severity :
3406+ validRotatingBackups . length > 0
3407+ ? "ok"
3408+ : rotatingBackups . length > 0
3409+ ? "error"
3410+ : "warn" ,
3411+ message :
3412+ validRotatingBackups . length > 0
3413+ ? `${ validRotatingBackups . length } rotating backup(s) available`
3414+ : rotatingBackups . length > 0
3415+ ? "Rotating backups are unreadable"
3416+ : "No rotating backups found yet" ,
3417+ details :
3418+ invalidRotatingBackups . length > 0
3419+ ? `${ invalidRotatingBackups . length } invalid backup(s); recreate by saving accounts`
3420+ : dirname ( storagePath ) ,
3421+ } ) ;
3422+
3423+ const snapshotBackups = await listAccountSnapshots ( ) ;
3424+ const validSnapshots = snapshotBackups . filter ( ( snapshot ) => snapshot . valid ) ;
3425+ const invalidSnapshots = snapshotBackups . filter ( ( snapshot ) => ! snapshot . valid ) ;
3426+ addCheck ( {
3427+ key : "snapshot-backups" ,
3428+ severity :
3429+ validSnapshots . length > 0
3430+ ? "ok"
3431+ : snapshotBackups . length > 0
3432+ ? "error"
3433+ : "warn" ,
3434+ message :
3435+ validSnapshots . length > 0
3436+ ? `${ validSnapshots . length } recovery snapshot(s) available`
3437+ : snapshotBackups . length > 0
3438+ ? "Snapshot backups are unreadable"
3439+ : "No recovery snapshots found" ,
3440+ details :
3441+ invalidSnapshots . length > 0
3442+ ? `${ invalidSnapshots . length } invalid snapshot(s); create a fresh snapshot before destructive actions`
3443+ : getNamedBackupsDirectoryPath ( ) ,
3444+ } ) ;
3445+
3446+ addCheck ( {
3447+ key : "recovery-chain" ,
3448+ severity :
3449+ storageExists ||
3450+ walExists ||
3451+ validRotatingBackups . length > 0 ||
3452+ validSnapshots . length > 0
3453+ ? "ok"
3454+ : "warn" ,
3455+ message :
3456+ storageExists ||
3457+ walExists ||
3458+ validRotatingBackups . length > 0 ||
3459+ validSnapshots . length > 0
3460+ ? "Recovery artifacts present"
3461+ : "No recovery artifacts found; create a snapshot or backup before destructive actions" ,
3462+ details : `storage=${ storageExists } , wal=${ walExists } , rotating=${ validRotatingBackups . length } , snapshots=${ validSnapshots . length } ` ,
3463+ } ) ;
3464+
33843465 const codexAuthPath = getCodexCliAuthPath ( ) ;
33853466 const codexConfigPath = getCodexCliConfigPath ( ) ;
33863467 let codexAuthEmail : string | undefined ;
@@ -3485,6 +3566,56 @@ async function runDoctor(args: string[]): Promise<number> {
34853566 } ) ;
34863567
34873568 const storage = await loadAccounts ( ) ;
3569+ const rollbackPlan = await getLatestCodexCliSyncRollbackPlan ( ) ;
3570+ if ( rollbackPlan . status === "ready" ) {
3571+ const accountCount =
3572+ rollbackPlan . accountCount ?? rollbackPlan . storage ?. accounts . length ;
3573+ addCheck ( {
3574+ key : "codex-cli-rollback-checkpoint" ,
3575+ severity : "ok" ,
3576+ message : `Latest manual Codex CLI rollback checkpoint ready (${ accountCount ?? "?" } account${ accountCount === 1 ? "" : "s" } )` ,
3577+ details : rollbackPlan . snapshot ?. path ,
3578+ } ) ;
3579+ } else {
3580+ const isBlocked = Boolean ( rollbackPlan . snapshot ) ;
3581+ addCheck ( {
3582+ key : "codex-cli-rollback-checkpoint" ,
3583+ severity : isBlocked ? "error" : "warn" ,
3584+ message : isBlocked
3585+ ? "Latest manual Codex CLI rollback checkpoint cannot be restored"
3586+ : "No manual Codex CLI rollback checkpoint has been recorded yet" ,
3587+ details : [
3588+ rollbackPlan . snapshot ?. path ?? rollbackPlan . snapshot ?. name ,
3589+ rollbackPlan . reason ,
3590+ isBlocked
3591+ ? "Action: Recreate the rollback checkpoint with a fresh manual Codex CLI sync before attempting rollback."
3592+ : "Action: Run a manual Codex CLI sync with backups enabled to capture a rollback checkpoint before applying changes." ,
3593+ ]
3594+ . filter ( Boolean )
3595+ . join ( " | " ) ,
3596+ } ) ;
3597+ }
3598+
3599+ const actionableNamedBackupRestores = await getActionableNamedBackupRestores ( {
3600+ currentStorage : storage ,
3601+ } ) ;
3602+ const actionableBackupCount = actionableNamedBackupRestores . assessments . length ;
3603+ addCheck ( {
3604+ key : "named-backup-restores" ,
3605+ severity : actionableBackupCount > 0 ? "ok" : "warn" ,
3606+ message :
3607+ actionableBackupCount > 0
3608+ ? `Found ${ actionableBackupCount } actionable named backup restore${ actionableBackupCount === 1 ? "" : "s" } `
3609+ : "No actionable named backup restores available" ,
3610+ details : [
3611+ `total backups: ${ actionableNamedBackupRestores . totalBackups } ` ,
3612+ actionableBackupCount > 0
3613+ ? undefined
3614+ : `Action: Add or copy a named backup into ${ getNamedBackupsDirectoryPath ( ) } before attempting recovery.` ,
3615+ ]
3616+ . filter ( Boolean )
3617+ . join ( " | " ) ,
3618+ } ) ;
34883619 let fixChanged = false ;
34893620 let fixActions : DoctorFixAction [ ] = [ ] ;
34903621 if ( options . fix && storage && storage . accounts . length > 0 ) {
0 commit comments