/ src / utils / appleTerminalBackup.ts
appleTerminalBackup.ts
  1  import { stat } from 'fs/promises'
  2  import { homedir } from 'os'
  3  import { join } from 'path'
  4  import { getGlobalConfig, saveGlobalConfig } from './config.js'
  5  import { execFileNoThrow } from './execFileNoThrow.js'
  6  import { logError } from './log.js'
  7  export function markTerminalSetupInProgress(backupPath: string): void {
  8    saveGlobalConfig(current => ({
  9      ...current,
 10      appleTerminalSetupInProgress: true,
 11      appleTerminalBackupPath: backupPath,
 12    }))
 13  }
 14  
 15  export function markTerminalSetupComplete(): void {
 16    saveGlobalConfig(current => ({
 17      ...current,
 18      appleTerminalSetupInProgress: false,
 19    }))
 20  }
 21  
 22  function getTerminalRecoveryInfo(): {
 23    inProgress: boolean
 24    backupPath: string | null
 25  } {
 26    const config = getGlobalConfig()
 27    return {
 28      inProgress: config.appleTerminalSetupInProgress ?? false,
 29      backupPath: config.appleTerminalBackupPath || null,
 30    }
 31  }
 32  
 33  export function getTerminalPlistPath(): string {
 34    return join(homedir(), 'Library', 'Preferences', 'com.apple.Terminal.plist')
 35  }
 36  
 37  export async function backupTerminalPreferences(): Promise<string | null> {
 38    const terminalPlistPath = getTerminalPlistPath()
 39    const backupPath = `${terminalPlistPath}.bak`
 40  
 41    try {
 42      const { code } = await execFileNoThrow('defaults', [
 43        'export',
 44        'com.apple.Terminal',
 45        terminalPlistPath,
 46      ])
 47  
 48      if (code !== 0) {
 49        return null
 50      }
 51  
 52      try {
 53        await stat(terminalPlistPath)
 54      } catch {
 55        return null
 56      }
 57  
 58      await execFileNoThrow('defaults', [
 59        'export',
 60        'com.apple.Terminal',
 61        backupPath,
 62      ])
 63  
 64      markTerminalSetupInProgress(backupPath)
 65  
 66      return backupPath
 67    } catch (error) {
 68      logError(error)
 69      return null
 70    }
 71  }
 72  
 73  type RestoreResult =
 74    | {
 75        status: 'restored' | 'no_backup'
 76      }
 77    | {
 78        status: 'failed'
 79        backupPath: string
 80      }
 81  
 82  export async function checkAndRestoreTerminalBackup(): Promise<RestoreResult> {
 83    const { inProgress, backupPath } = getTerminalRecoveryInfo()
 84    if (!inProgress) {
 85      return { status: 'no_backup' }
 86    }
 87  
 88    if (!backupPath) {
 89      markTerminalSetupComplete()
 90      return { status: 'no_backup' }
 91    }
 92  
 93    try {
 94      await stat(backupPath)
 95    } catch {
 96      markTerminalSetupComplete()
 97      return { status: 'no_backup' }
 98    }
 99  
100    try {
101      const { code } = await execFileNoThrow('defaults', [
102        'import',
103        'com.apple.Terminal',
104        backupPath,
105      ])
106  
107      if (code !== 0) {
108        return { status: 'failed', backupPath }
109      }
110  
111      await execFileNoThrow('killall', ['cfprefsd'])
112  
113      markTerminalSetupComplete()
114      return { status: 'restored' }
115    } catch (restoreError) {
116      logError(
117        new Error(
118          `Failed to restore Terminal.app settings with: ${restoreError}`,
119        ),
120      )
121      markTerminalSetupComplete()
122      return { status: 'failed', backupPath }
123    }
124  }