Files
BIPMaterialManager/src/main/services/config/config-manager.ts
Misaka_Company f3be0ad01d feat: implement savePartialSettings with validation and rollback
Implements the core savePartialSettings method that:
- Validates fields against UI_EDITABLE_FIELDS whitelist
- Deep merges partial updates with current settings
- Creates backup before saving
- Restores backup on save failure
- Reloads .env file to populate cache with correct keys

Added comprehensive tests:
- Partial update preserves existing fields
- Rejects non-whitelisted fields
- Handles nested object updates
- Restores backup on save failure

Fixed cache key inconsistency bug by clearing cache in loadEnvFile()
and reloading after save to ensure proper cache population.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-03 14:36:50 +08:00

708 lines
24 KiB
TypeScript

/**
* Configuration Manager
*
* Manages application configuration stored in .env file
* Provides methods for reading, writing, and saving configuration values
*/
import * as fs from 'fs'
import * as path from 'path'
import { fileURLToPath } from 'url'
import { dirname } from 'path'
import { createLogger } from '../logger'
import type {
SettingsData,
DatabaseType,
MatchMode,
ValidationDataSource
} from '../../types/settings.types'
const log = createLogger('ConfigManager')
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
/**
* Default settings values
*/
const DEFAULT_SETTINGS: SettingsData = {
erp: {
url: 'https://68.11.34.30:8082/',
username: '',
password: '',
headless: true,
ignoreHttpsErrors: true,
autoCloseBrowser: true
},
database: {
dbType: 'mysql',
server: '',
mysqlHost: '192.168.31.83',
mysqlPort: 3306,
database: 'BLD_DB',
username: 'remote_user',
password: ''
},
paths: {
dataDir: 'D:/python/playwrite/data/',
defaultOutput: '离散备料计划维护_合并.xlsx',
validationOutput: '物料状态校验结果.xlsx'
},
extraction: {
batchSize: 100,
verbose: true,
autoConvert: true,
mergeBatches: true,
enableDbPersistence: true
},
validation: {
dataSource: 'database_full',
batchSize: 2000,
matchMode: 'substring',
enableCrud: false,
defaultManager: ''
},
ui: {
fontFamily: 'Microsoft YaHei UI',
fontSize: 10,
productionIdInputWidth: 20
},
execution: {
dryRun: false
}
}
/**
* Check if value is a plain object
*/
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/**
* Deep merge two objects, only updating fields present in target
* Preserves all fields from source that are not in target
*/
function deepMerge<T>(source: T, target: Partial<T>): T {
const result = { ...source }
for (const key in target) {
if (key in target) {
const targetValue = target[key]
const sourceValue = result[key]
if (isObject(targetValue) && isObject(sourceValue)) {
result[key] = deepMerge(
sourceValue as T[Extract<keyof T, string>],
targetValue as Partial<T[Extract<keyof T, string>]>
)
} else if (targetValue !== undefined) {
result[key] = targetValue as T[Extract<keyof T, string>]
}
}
}
return result
}
/**
* UI editable field whitelist
* Fields that can be modified through the settings UI
*/
const UI_EDITABLE_FIELDS: string[] = [
'erp.url',
'erp.username',
'erp.password'
// Add more fields as UI expands
]
/**
* Validate that settings only contain editable fields
*/
function validateEditableFields(settings: Partial<SettingsData>): {
valid: boolean
invalidFields: string[]
} {
const invalidFields: string[] = []
for (const [section, values] of Object.entries(settings)) {
if (values && typeof values === 'object') {
for (const field of Object.keys(values)) {
const fieldPath = `${section}.${field}`
if (!UI_EDITABLE_FIELDS.includes(fieldPath)) {
invalidFields.push(fieldPath)
}
}
}
}
return {
valid: invalidFields.length === 0,
invalidFields
}
}
/**
* Configuration Manager Class
*/
export class ConfigManager {
private static instance: ConfigManager | null = null
private envPath: string
private backupPath: string
private configCache: Map<string, string> = new Map()
private initialized: boolean = false
private constructor() {
if (this.initialized) {
return
}
this.envPath = path.resolve(__dirname, '../../.env')
this.backupPath = path.resolve(__dirname, '../../.env.backup')
this.initialized = true
}
/**
* Get the singleton instance
*/
public static getInstance(): ConfigManager {
if (ConfigManager.instance === null) {
ConfigManager.instance = new ConfigManager()
}
return ConfigManager.instance
}
/**
* Initialize configuration from .env file
*/
public async initialize(): Promise<void> {
await this.loadEnvFile()
}
/**
* Load .env file into cache
*/
private async loadEnvFile(): Promise<void> {
try {
// Clear cache before loading
this.configCache.clear()
if (fs.existsSync(this.envPath)) {
const content = fs.readFileSync(this.envPath, 'utf-8')
const lines = content.split('\n')
for (const line of lines) {
const trimmedLine = line.trim()
// Skip empty lines and comments
if (!trimmedLine || trimmedLine.startsWith('#')) {
continue
}
const [key, ...valueParts] = trimmedLine.split('=')
if (key && valueParts.length > 0) {
const value = valueParts.join('=').trim()
this.configCache.set(key.trim(), value)
}
}
}
} catch (error) {
console.error('[ConfigManager] Failed to load .env file:', error)
}
}
/**
* Get a configuration value
* @param key - Configuration key
* @param defaultValue - Default value if key doesn't exist
*/
public get(key: string, defaultValue?: string): string | undefined {
return this.configCache.get(key) ?? defaultValue
}
/**
* Get a boolean configuration value
*/
public getBoolean(key: string, defaultValue: boolean = false): boolean {
const value = this.get(key)
if (value === undefined) return defaultValue
return value.toLowerCase() === 'true'
}
/**
* Get a number configuration value
*/
public getNumber(key: string, defaultValue: number = 0): number {
const value = this.get(key)
if (value === undefined) return defaultValue
const parsed = parseInt(value, 10)
return isNaN(parsed) ? defaultValue : parsed
}
/**
* Set a configuration value in cache
*/
public set(key: string, value: string | number | boolean): void {
this.configCache.set(key, String(value))
}
/**
* Save configuration to .env file
*/
public async save(): Promise<boolean> {
try {
// Build .env content from cache
const lines: string[] = []
// ERP Configuration
lines.push('# ===========================')
lines.push('# ERP 系统配置')
lines.push('# ===========================')
lines.push(`ERP_URL=${this.configCache.get('erp.url') || DEFAULT_SETTINGS.erp.url}`)
lines.push(
`ERP_USERNAME=${this.configCache.get('erp.username') || DEFAULT_SETTINGS.erp.username}`
)
lines.push(
`ERP_PASSWORD=${this.configCache.get('erp.password') || DEFAULT_SETTINGS.erp.password}`
)
lines.push(
`ERP_HEADLESS=${this.configCache.get('erp.headless') || DEFAULT_SETTINGS.erp.headless}`
)
lines.push(
`ERP_IGNORE_HTTPS_ERRORS=${this.configCache.get('erp.ignoreHttpsErrors') || DEFAULT_SETTINGS.erp.ignoreHttpsErrors}`
)
lines.push(
`ERP_AUTO_CLOSE_BROWSER=${this.configCache.get('erp.autoCloseBrowser') || DEFAULT_SETTINGS.erp.autoCloseBrowser}`
)
lines.push('')
// Database Configuration - SQL Server
lines.push('# ===========================')
lines.push('# 数据库配置 - SQL Server')
lines.push('# ===========================')
lines.push(`# DB_TYPE=sqlserver`)
lines.push(`# DB_SERVER=${this.configCache.get('database.server') || ''}`)
lines.push(`# DB_NAME=${this.configCache.get('database.database') || ''}`)
lines.push(`# DB_USERNAME=${this.configCache.get('database.username') || ''}`)
lines.push(`# DB_PASSWORD=${this.configCache.get('database.password') || ''}`)
lines.push(`DB_SQLSERVER_DRIVER=ODBC Driver 18 for SQL Server`)
lines.push(`DB_TRUST_SERVER_CERTIFICATE=yes`)
lines.push('')
// Database Configuration - MySQL
lines.push('# ===========================')
lines.push('# 数据库配置 - MySQL (切换时使用)')
lines.push('# ===========================')
lines.push(
`DB_TYPE=${this.configCache.get('database.dbType') || DEFAULT_SETTINGS.database.dbType}`
)
lines.push(
`DB_NAME=${this.configCache.get('database.database') || DEFAULT_SETTINGS.database.database}`
)
lines.push(
`DB_USERNAME=${this.configCache.get('database.username') || DEFAULT_SETTINGS.database.username}`
)
lines.push(
`DB_PASSWORD=${this.configCache.get('database.password') || DEFAULT_SETTINGS.database.password}`
)
lines.push(
`DB_MYSQL_HOST=${this.configCache.get('database.mysqlHost') || DEFAULT_SETTINGS.database.mysqlHost}`
)
lines.push(
`DB_MYSQL_PORT=${this.configCache.get('database.mysqlPort') || DEFAULT_SETTINGS.database.mysqlPort}`
)
lines.push(`DB_MYSQL_CHARSET=utf8mb4`)
lines.push('')
// Order number parsing table configuration
lines.push('# 订单号解析表配置')
lines.push('# 表名:包含 productionID 和 生产订单号 映射关系的表')
lines.push(`DB_TABLE_NAME=productionContractData_26 年压力表合同数据`)
lines.push('# 字段名:总排号 (对应 productionID)')
lines.push(`DB_FIELD_PRODUCTION_ID=总排号`)
lines.push('# 字段名:生产订单号 (对应生产订单号)')
lines.push(`DB_FIELD_ORDER_NUMBER=生产订单号`)
lines.push('')
// Path Configuration
lines.push('# ===========================')
lines.push('# 路径配置')
lines.push('# ===========================')
lines.push(
`PATH_DATA_DIR=${this.configCache.get('paths.dataDir') || DEFAULT_SETTINGS.paths.dataDir}`
)
lines.push(`PATH_PRODUCTION_ID_FILE=ProductionID.txt`)
lines.push(
`PATH_DEFAULT_OUTPUT=${this.configCache.get('paths.defaultOutput') || DEFAULT_SETTINGS.paths.defaultOutput}`
)
lines.push(
`PATH_VALIDATION_OUTPUT=${this.configCache.get('paths.validationOutput') || DEFAULT_SETTINGS.paths.validationOutput}`
)
lines.push('')
// Data Extraction Configuration
lines.push('# ===========================')
lines.push('# 数据提取配置')
lines.push('# ===========================')
lines.push(
`EXTRACTION_BATCH_SIZE=${this.configCache.get('extraction.batchSize') || DEFAULT_SETTINGS.extraction.batchSize}`
)
lines.push(
`EXTRACTION_VERBOSE=${this.configCache.get('extraction.verbose') || DEFAULT_SETTINGS.extraction.verbose}`
)
lines.push(
`EXTRACTION_AUTO_CONVERT=${this.configCache.get('extraction.autoConvert') || DEFAULT_SETTINGS.extraction.autoConvert}`
)
lines.push(
`EXTRACTION_MERGE_BATCHES=${this.configCache.get('extraction.mergeBatches') || DEFAULT_SETTINGS.extraction.mergeBatches}`
)
lines.push(
`EXTRACTION_ENABLE_DB_PERSISTENCE=${this.configCache.get('extraction.enableDbPersistence') || DEFAULT_SETTINGS.extraction.enableDbPersistence}`
)
lines.push('')
// Validation Configuration
lines.push('# ===========================')
lines.push('# 校验配置')
lines.push('# ===========================')
lines.push(
`VALIDATION_DATA_SOURCE=${this.configCache.get('validation.dataSource') || DEFAULT_SETTINGS.validation.dataSource}`
)
lines.push(
`VALIDATION_USE_DATABASE=${this.configCache.get('validation.useDatabase') || true}`
)
lines.push(
`VALIDATION_BATCH_SIZE=${this.configCache.get('validation.batchSize') || DEFAULT_SETTINGS.validation.batchSize}`
)
lines.push(
`VALIDATION_ENABLE_CRUD=${this.configCache.get('validation.enableCrud') || DEFAULT_SETTINGS.validation.enableCrud}`
)
lines.push(
`VALIDATION_DEFAULT_MANAGER=${this.configCache.get('validation.defaultManager') || DEFAULT_SETTINGS.validation.defaultManager}`
)
lines.push(
`VALIDATION_MATCH_MODE=${this.configCache.get('validation.matchMode') || DEFAULT_SETTINGS.validation.matchMode}`
)
lines.push('')
// UI Configuration
lines.push('# ===========================')
lines.push('# UI 配置')
lines.push('# ===========================')
lines.push(
`UI_FONT_FAMILY=${this.configCache.get('ui.fontFamily') || DEFAULT_SETTINGS.ui.fontFamily}`
)
lines.push(
`UI_FONT_SIZE=${this.configCache.get('ui.fontSize') || DEFAULT_SETTINGS.ui.fontSize}`
)
lines.push(
`UI_PRODUCTION_ID_INPUT_WIDTH=${this.configCache.get('ui.productionIdInputWidth') || DEFAULT_SETTINGS.ui.productionIdInputWidth}`
)
lines.push('')
// Execution Configuration
lines.push('# ===========================')
lines.push('# 执行配置')
lines.push('# ===========================')
lines.push(
`EXECUTION_DRYRUN=${this.configCache.get('execution.dryRun') || DEFAULT_SETTINGS.execution.dryRun}`
)
const content = lines.join('\n')
fs.writeFileSync(this.envPath, content, 'utf-8')
return true
} catch (error) {
console.error('[ConfigManager] Failed to save .env file:', error)
return false
}
}
/**
* Get all settings as SettingsData object
*/
public getAllSettings(): SettingsData {
return {
erp: {
url: this.get('ERP_URL', DEFAULT_SETTINGS.erp.url),
username: this.get('ERP_USERNAME', DEFAULT_SETTINGS.erp.username),
password: this.get('ERP_PASSWORD', DEFAULT_SETTINGS.erp.password),
headless: this.getBoolean('ERP_HEADLESS', DEFAULT_SETTINGS.erp.headless),
ignoreHttpsErrors: this.getBoolean(
'ERP_IGNORE_HTTPS_ERRORS',
DEFAULT_SETTINGS.erp.ignoreHttpsErrors
),
autoCloseBrowser: this.getBoolean(
'ERP_AUTO_CLOSE_BROWSER',
DEFAULT_SETTINGS.erp.autoCloseBrowser
)
},
database: {
dbType:
(this.get('DB_TYPE', DEFAULT_SETTINGS.database.dbType) as DatabaseType) ||
DEFAULT_SETTINGS.database.dbType,
server: this.get('DB_SERVER', DEFAULT_SETTINGS.database.server),
mysqlHost: this.get('DB_MYSQL_HOST', DEFAULT_SETTINGS.database.mysqlHost),
mysqlPort: this.getNumber('DB_MYSQL_PORT', DEFAULT_SETTINGS.database.mysqlPort),
database: this.get('DB_NAME', DEFAULT_SETTINGS.database.database),
username: this.get('DB_USERNAME', DEFAULT_SETTINGS.database.username),
password: this.get('DB_PASSWORD', DEFAULT_SETTINGS.database.password)
},
paths: {
dataDir: this.get('PATH_DATA_DIR', DEFAULT_SETTINGS.paths.dataDir),
defaultOutput: this.get('PATH_DEFAULT_OUTPUT', DEFAULT_SETTINGS.paths.defaultOutput),
validationOutput: this.get(
'PATH_VALIDATION_OUTPUT',
DEFAULT_SETTINGS.paths.validationOutput
)
},
extraction: {
batchSize: this.getNumber('EXTRACTION_BATCH_SIZE', DEFAULT_SETTINGS.extraction.batchSize),
verbose: this.getBoolean('EXTRACTION_VERBOSE', DEFAULT_SETTINGS.extraction.verbose),
autoConvert: this.getBoolean(
'EXTRACTION_AUTO_CONVERT',
DEFAULT_SETTINGS.extraction.autoConvert
),
mergeBatches: this.getBoolean(
'EXTRACTION_MERGE_BATCHES',
DEFAULT_SETTINGS.extraction.mergeBatches
),
enableDbPersistence: this.getBoolean(
'EXTRACTION_ENABLE_DB_PERSISTENCE',
DEFAULT_SETTINGS.extraction.enableDbPersistence
)
},
validation: {
dataSource:
(this.get(
'VALIDATION_DATA_SOURCE',
DEFAULT_SETTINGS.validation.dataSource
) as ValidationDataSource) || DEFAULT_SETTINGS.validation.dataSource,
batchSize: this.getNumber('VALIDATION_BATCH_SIZE', DEFAULT_SETTINGS.validation.batchSize),
matchMode:
(this.get('VALIDATION_MATCH_MODE', DEFAULT_SETTINGS.validation.matchMode) as MatchMode) ||
DEFAULT_SETTINGS.validation.matchMode,
enableCrud: this.getBoolean(
'VALIDATION_ENABLE_CRUD',
DEFAULT_SETTINGS.validation.enableCrud
),
defaultManager: this.get(
'VALIDATION_DEFAULT_MANAGER',
DEFAULT_SETTINGS.validation.defaultManager
)
},
ui: {
fontFamily: this.get('UI_FONT_FAMILY', DEFAULT_SETTINGS.ui.fontFamily),
fontSize: this.getNumber('UI_FONT_SIZE', DEFAULT_SETTINGS.ui.fontSize),
productionIdInputWidth: this.getNumber(
'UI_PRODUCTION_ID_INPUT_WIDTH',
DEFAULT_SETTINGS.ui.productionIdInputWidth
)
},
execution: {
dryRun: this.getBoolean('EXECUTION_DRYRUN', DEFAULT_SETTINGS.execution.dryRun)
}
}
}
/**
* Save settings from SettingsData object
*/
public async saveAllSettings(settings: SettingsData): Promise<boolean> {
// ERP settings
this.set('erp.url', settings.erp.url)
this.set('erp.username', settings.erp.username)
this.set('erp.password', settings.erp.password)
this.set('erp.headless', settings.erp.headless)
this.set('erp.ignoreHttpsErrors', settings.erp.ignoreHttpsErrors)
this.set('erp.autoCloseBrowser', settings.erp.autoCloseBrowser)
// Database settings
this.set('database.dbType', settings.database.dbType)
this.set('database.server', settings.database.server)
this.set('database.mysqlHost', settings.database.mysqlHost)
this.set('database.mysqlPort', settings.database.mysqlPort)
this.set('database.database', settings.database.database)
this.set('database.username', settings.database.username)
this.set('database.password', settings.database.password)
// Path settings
this.set('paths.dataDir', settings.paths.dataDir)
this.set('paths.defaultOutput', settings.paths.defaultOutput)
this.set('paths.validationOutput', settings.paths.validationOutput)
// Extraction settings
this.set('extraction.batchSize', settings.extraction.batchSize)
this.set('extraction.verbose', settings.extraction.verbose)
this.set('extraction.autoConvert', settings.extraction.autoConvert)
this.set('extraction.mergeBatches', settings.extraction.mergeBatches)
this.set('extraction.enableDbPersistence', settings.extraction.enableDbPersistence)
// Validation settings
this.set('validation.dataSource', settings.validation.dataSource)
this.set('validation.batchSize', settings.validation.batchSize)
this.set('validation.matchMode', settings.validation.matchMode)
this.set('validation.enableCrud', settings.validation.enableCrud)
this.set('validation.defaultManager', settings.validation.defaultManager)
// UI settings
this.set('ui.fontFamily', settings.ui.fontFamily)
this.set('ui.fontSize', settings.ui.fontSize)
this.set('ui.productionIdInputWidth', settings.ui.productionIdInputWidth)
// Execution settings
this.set('execution.dryRun', settings.execution.dryRun)
return this.save()
}
/**
* Save partial settings (only update provided fields)
* Preserves all existing fields not included in the update
*/
public async savePartialSettings(
settings: Partial<SettingsData>
): Promise<{ success: boolean; error?: string }> {
try {
// Step 1: Validate field whitelist
const validation = validateEditableFields(settings)
if (!validation.valid) {
log.warn('Attempted to save non-editable fields', {
invalidFields: validation.invalidFields
})
return {
success: false,
error: `包含不允许修改的字段:${validation.invalidFields.join(', ')}`
}
}
// Step 2: Read current settings
const currentSettings = this.getAllSettings()
// Step 3: Deep merge
const mergedSettings = deepMerge(currentSettings, settings)
// Step 4: Backup and save
const backupSuccess = await this.backupEnvFile()
if (!backupSuccess) {
log.warn('Failed to backup .env file, proceeding with caution')
}
const saveSuccess = await this.saveAllSettings(mergedSettings)
if (!saveSuccess) {
// Save failed, attempt restore
await this.restoreBackup()
return {
success: false,
error: '保存配置失败,已恢复原配置'
}
}
// Step 5: Reload from disk to populate cache with correct keys (ERP_URL instead of erp.url)
await this.loadEnvFile()
log.info('Settings saved successfully', {
updatedFields: Object.keys(settings)
})
return { success: true }
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Error in savePartialSettings', { error: message })
await this.restoreBackup()
return {
success: false,
error: `保存配置时发生错误:${message}`
}
}
}
/**
* Reset to default settings
*/
public resetToDefaults(): SettingsData {
// Clear cache and reload from defaults
this.configCache.clear()
// Set all defaults
this.set('erp.url', DEFAULT_SETTINGS.erp.url)
this.set('erp.username', DEFAULT_SETTINGS.erp.username)
this.set('erp.password', DEFAULT_SETTINGS.erp.password)
this.set('erp.headless', DEFAULT_SETTINGS.erp.headless)
this.set('erp.ignoreHttpsErrors', DEFAULT_SETTINGS.erp.ignoreHttpsErrors)
this.set('erp.autoCloseBrowser', DEFAULT_SETTINGS.erp.autoCloseBrowser)
this.set('database.dbType', DEFAULT_SETTINGS.database.dbType)
this.set('database.server', DEFAULT_SETTINGS.database.server)
this.set('database.mysqlHost', DEFAULT_SETTINGS.database.mysqlHost)
this.set('database.mysqlPort', DEFAULT_SETTINGS.database.mysqlPort)
this.set('database.database', DEFAULT_SETTINGS.database.database)
this.set('database.username', DEFAULT_SETTINGS.database.username)
this.set('database.password', DEFAULT_SETTINGS.database.password)
this.set('paths.dataDir', DEFAULT_SETTINGS.paths.dataDir)
this.set('paths.defaultOutput', DEFAULT_SETTINGS.paths.defaultOutput)
this.set('paths.validationOutput', DEFAULT_SETTINGS.paths.validationOutput)
this.set('extraction.batchSize', DEFAULT_SETTINGS.extraction.batchSize)
this.set('extraction.verbose', DEFAULT_SETTINGS.extraction.verbose)
this.set('extraction.autoConvert', DEFAULT_SETTINGS.extraction.autoConvert)
this.set('extraction.mergeBatches', DEFAULT_SETTINGS.extraction.mergeBatches)
this.set('extraction.enableDbPersistence', DEFAULT_SETTINGS.extraction.enableDbPersistence)
this.set('validation.dataSource', DEFAULT_SETTINGS.validation.dataSource)
this.set('validation.batchSize', DEFAULT_SETTINGS.validation.batchSize)
this.set('validation.matchMode', DEFAULT_SETTINGS.validation.matchMode)
this.set('validation.enableCrud', DEFAULT_SETTINGS.validation.enableCrud)
this.set('validation.defaultManager', DEFAULT_SETTINGS.validation.defaultManager)
this.set('ui.fontFamily', DEFAULT_SETTINGS.ui.fontFamily)
this.set('ui.fontSize', DEFAULT_SETTINGS.ui.fontSize)
this.set('ui.productionIdInputWidth', DEFAULT_SETTINGS.ui.productionIdInputWidth)
this.set('execution.dryRun', DEFAULT_SETTINGS.execution.dryRun)
return DEFAULT_SETTINGS
}
/**
* Get default settings
*/
public getDefaultSettings(): SettingsData {
return DEFAULT_SETTINGS
}
/**
* Backup current .env file
*/
private async backupEnvFile(): Promise<boolean> {
try {
if (fs.existsSync(this.envPath)) {
fs.copyFileSync(this.envPath, this.backupPath)
log.debug('Backup created', { path: this.backupPath })
return true
}
return false
} catch (error) {
log.error('Failed to backup .env file', { error })
return false
}
}
/**
* Restore .env file from backup
*/
private async restoreBackup(): Promise<boolean> {
try {
if (fs.existsSync(this.backupPath)) {
fs.copyFileSync(this.backupPath, this.envPath)
await this.loadEnvFile()
log.debug('Restored from backup')
return true
}
return false
} catch (error) {
log.error('Failed to restore backup', { error })
return false
}
}
}