UNPKG

i18ntk

Version:

i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.

1,135 lines (1,014 loc) • 94 kB
/** * Settings CLI Interface * Interactive terminal-based settings management for i18n toolkit * No external dependencies - uses Node.js built-in readline */ const cliHelper = require('../utils/cli-helper'); const fs = require('fs'); const path = require('path'); const SettingsManager = require('./settings-manager'); const settingsManager = new SettingsManager(); const UIi18n = require('../main/i18ntk-ui'); const configManager = require('../utils/config-manager'); const { loadTranslations, t } = require('../utils/i18n-helper'); loadTranslations(null, path.resolve(__dirname, '..', 'ui-locales')); const AdminAuth = require('../utils/admin-auth'); const uiI18n = new UIi18n(); // ANSI color codes for terminal output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', bgRed: '\x1b[41m', bgGreen: '\x1b[42m', bgYellow: '\x1b[43m' }; function isAdminPinEnabled() { const cfg = configManager.getConfig(); return cfg.security?.adminPinEnabled || false; } function printHelp() { console.log([ '', 'i18n Toolkit Settings CLI', '', 'Usage: node settings/settings-cli.js [options]', '', 'Options:', ' --help, -h Show this help message', ' --list-languages Print supported UI/source language codes', ' --language-status Print current language settings', '', 'Interactive Commands:', ' 1) UI Settings - Configure language, theme, and UI preferences', ' 2) Directory Settings - Set source and output directories', ' 3) Script Directories - Configure script-specific paths', ' 4) Processing Settings - Batch size, concurrency, thresholds', ' 5) Security Settings - Admin PIN, session management', ' 6) Advanced Settings - Strict mode, audit logging, backups', ' 7) View All Settings - Display current configuration', ' 8) Import/Export - Backup and restore settings', ' 9) Reset to Defaults - Restore factory settings', ' 0) Report Bug - Generate bug report', ' u) Update Package - Update i18n toolkit', ' s) Save Changes - Save current configuration', ' h) Help - Show available commands', ' q) Quit - Exit settings CLI', '', 'Examples:', ' node settings/settings-cli.js', ' npm run languages:list', ' npm run languages:status', '', 'Note: Run without options to open the interactive menu.', ].join('\n')); } function printAvailableLanguages() { console.log('Available languages:'); for (const language of settingsManager.getAvailableLanguages()) { console.log(` ${language.code} - ${language.name}`); } } function printLanguageStatus() { const settings = configManager.getConfig(); const availableLanguages = settingsManager.getAvailableLanguages().map(language => language.code); const available = new Set(availableLanguages); const uiLanguage = settings.uiLanguage || settings.language || 'en'; const sourceLanguage = settings.sourceLanguage || settings.language || 'en'; const targetLanguages = Array.isArray(settings.targetLanguages) ? settings.targetLanguages : Array.isArray(settings.defaultLanguages) ? settings.defaultLanguages : []; console.log('Language status:'); console.log(` UI language: ${uiLanguage}${available.has(uiLanguage) ? '' : ' (not in supported list)'}`); console.log(` Source language: ${sourceLanguage}${available.has(sourceLanguage) ? '' : ' (not in supported list)'}`); console.log(` Target languages: ${targetLanguages.length ? targetLanguages.join(', ') : '(none configured)'}`); console.log(` Supported languages: ${availableLanguages.join(', ')}`); } class SettingsCLI { constructor() { this.rl = null; // Use cliHelper instead this.settings = null; this.schema = null; this.modified = false; this.adminAuth = new AdminAuth(); this.adminAuthenticated = false; } /** * Initialize the CLI interface */ async init() { try { this.settings = configManager.getConfig(); this.schema = settingsManager.getSettingsSchema(); return true; } catch (error) { this.error(t('settings.initFailed', { error: error.message })); return false; } } /** * Start the interactive settings interface */ async start() { if (!(await this.init())) { process.exit(1); } this.clearScreen(); this.showHeader(); await this.showMainMenu(); } /** * Run the settings interface (alias for start) */ async run() { await this.start(); } /** * Clear the terminal screen */ clearScreen() { process.stdout.write('\x1b[2J\x1b[0f'); } /** * Show the main header */ showHeader() { // Refresh language from settings to ensure consistency if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } const title = t('operations.settings.title') || 'Settings Management'; const separator = t('operations.settings.separator') || '============================================================'; console.log(`${colors.cyan}${colors.bright}`); console.log(`${title}`); console.log(separator); console.log(colors.reset); if (this.modified) { console.log(`${colors.yellow}${t('settings.mainMenu.unsavedChangesWarning')}${colors.reset}\n`); } } /** * Show the main menu */ async showMainMenu() { // Check if admin PIN is configured for display purposes await this.adminAuth.initialize(); const config = await this.adminAuth.loadConfig(); const pinSet = config && !!config.pinHash; const protectionEnabled = isAdminPinEnabled(); const pinStatus = pinSet ? `${colors.green}āœ…${colors.reset}` : `${colors.red}āŒ${colors.reset}`; const options = [ { key: '1', label: t('settings.mainMenu.uiSettings'), description: t('settings.mainMenu.uiSettingsDesc') }, { key: '2', label: t('settings.mainMenu.directorySettings'), description: t('settings.mainMenu.directorySettingsDesc') }, { key: '3', label: t('settings.mainMenu.scriptDirectorySettings'), description: t('settings.mainMenu.scriptDirectorySettingsDesc') }, { key: '4', label: t('settings.mainMenu.processingSettings'), description: t('settings.mainMenu.processingSettingsDesc') }, { key: '5', label: t('settings.mainMenu.backupSettings'), description: t('settings.mainMenu.backupSettingsDesc') }, { key: '6', label: t('settings.mainMenu.securitySettings'), description: `${t('settings.mainMenu.securitySettingsDesc')} ${pinStatus}` }, { key: '7', label: t('settings.mainMenu.advancedSettings'), description: t('settings.mainMenu.advancedSettingsDesc') }, { key: '8', label: t('settings.mainMenu.viewAllSettings'), description: t('settings.mainMenu.viewAllSettingsDesc') }, { key: '9', label: t('settings.mainMenu.importExport'), description: t('settings.mainMenu.importExportDesc') }, { key: 'a', label: t('settings.mainMenu.autoTranslate'), description: t('settings.mainMenu.autoTranslateDesc') }, { key: '0', label: t('settings.mainMenu.reportBug'), description: t('settings.mainMenu.reportBugDesc') }, { key: 'x', label: 'Reset Script Directory Overrides', description: 'Clear script directory overrides and use defaults' }, { key: 'r', label: t('settings.mainMenu.resetToDefaults'), description: t('settings.mainMenu.resetToDefaultsDesc') }, { key: 'u', label: t('settings.mainMenu.updatePackage'), description: t('settings.mainMenu.updatePackageDesc') }, { key: 's', label: t('settings.mainMenu.saveChanges'), description: t('settings.mainMenu.saveChangesDesc') }, { key: 'h', label: t('settings.mainMenu.help'), description: t('settings.mainMenu.helpDesc') }, { key: 'q', label: t('settings.mainMenu.quit'), description: t('settings.mainMenu.quitDesc') } ]; console.log(`${colors.bright}${t('settings.mainMenu.title')}${colors.reset}\n`); options.forEach(option => { const keyColor = option.key.match(/[0-9]/) ? colors.cyan : colors.yellow; console.log(` ${keyColor}${option.key}${colors.reset}) ${colors.bright}${option.label}${colors.reset}`); console.log(` ${colors.dim}${option.description}${colors.reset}`); }); console.log(); const choice = await this.prompt(t('settings.mainMenu.selectOption')); await this.handleMainMenuChoice(choice.toLowerCase()); } /** * Handle main menu choice */ async handleMainMenuChoice(choice) { switch (choice) { case '1': await this.showUISettings(); break; case '2': await this.showDirectorySettings(); break; case '3': await this.showScriptDirectorySettings(); break; case '4': await this.showProcessingSettings(); break; case '5': await this.showBackupSettings(); break; case '6': await this.showSecuritySettings(); break; case '7': await this.showAdvancedSettings(); break; case '8': await this.showAllSettings(); break; case '9': await this.showImportExport(); break; case 'a': await this.showAutoTranslateSettings(); break; case '0': await this.reportBug(); break; case 'x': await this.resetScriptDirectories(); break; case 'r': await this.resetToDefaults(); break; case 'u': await this.updatePackage(); break; case 's': await this.saveSettings(); break; case 'h': await this.showHelp(); break; case 'q': await this.quit(); return; default: this.error('Invalid option. Please try again.'); await this.pause(); break; } this.clearScreen(); this.showHeader(); await this.showMainMenu(); } /** * Show UI settings menu */ async showUISettings() { // Refresh language from settings to ensure consistency if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } this.clearScreen(); this.showHeader(); console.log(`${colors.bright}${t('settings.categories.uiSettings')}${colors.reset}\n`); const uiSettings = { 'dateFormat': t('settings.fields.dateFormat.label'), 'notifications.enabled': t('settings.fields.notifications.enabled.label'), 'removeUiLanguages': t('settings.fields.removeUiLanguages.label') }; await this.showSettingsCategory(uiSettings); } /** * Show directory settings menu */ async showDirectorySettings() { // Refresh language from settings to ensure consistency if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } this.clearScreen(); this.showHeader(); console.log(`${colors.bright}${t('settings.categories.directorySettings')}${colors.reset}\n`); console.log(`šŸ“ ${colors.cyan}${t('settings.currentDirectory')}: ${process.cwd()}${colors.reset}`); console.log(`šŸ’” ${colors.dim}${t('settings.relativePathHint')}${colors.reset}\n`); const dirSettings = { 'projectRoot': t('settings.fields.projectRoot.label'), 'sourceDir': t('settings.fields.sourceDir.label'), 'i18nDir': t('settings.fields.i18nDir.label'), 'outputDir': t('settings.fields.outputDir.label') }; await this.showSettingsCategory(dirSettings); } /** * Show script-specific directory settings menu */ async showScriptDirectorySettings() { // Refresh language from settings to ensure consistency if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } this.clearScreen(); this.showHeader(); console.log(`${colors.bright}${t('settings.categories.scriptDirectorySettings')}${colors.reset}\n`); console.log(`šŸ“ ${colors.cyan}${t('settings.currentDirectory')}: ${process.cwd()}${colors.reset}`); console.log(`šŸ’” ${colors.dim}${t('settings.relativePathHint')}${colors.reset}\n`); const scriptDirSettings = { 'scriptDirectories.analyze': t('settings.fields.scriptDirectories.analyzeLabel'), 'scriptDirectories.complete': t('settings.fields.scriptDirectories.completeLabel'), 'scriptDirectories.init': t('settings.fields.scriptDirectories.initLabel'), 'scriptDirectories.manage': t('settings.fields.scriptDirectories.manageLabel'), 'scriptDirectories.sizing': t('settings.fields.scriptDirectories.sizingLabel'), 'scriptDirectories.summary': t('settings.fields.scriptDirectories.summaryLabel'), 'scriptDirectories.usage': t('settings.fields.scriptDirectories.usageLabel'), 'scriptDirectories.validate': t('settings.fields.scriptDirectories.validateLabel') }; await this.showSettingsCategory(scriptDirSettings); } /** * Show processing settings menu */ async showProcessingSettings() { // Refresh language from settings to ensure consistency if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } this.clearScreen(); this.showHeader(); console.log(`${colors.bright}${t('settings.categories.processingSettings')}${colors.reset}\n`); const processSettings = { 'advanced.batchSize': t('settings.fields.batchSize.label'), 'advanced.maxConcurrentFiles': t('settings.fields.maxConcurrentFiles.label'), 'advanced.sizingThreshold': t('settings.fields.sizingThreshold.label') }; await this.showSettingsCategory(processSettings); } async showAutoTranslateSettings() { if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } this.clearScreen(); this.showHeader(); console.log(`${colors.bright}${t('settings.categories.autoTranslate')}${colors.reset}\n`); const autoTranslateSettings = { 'autoTranslate.placeholderMode': t('settings.fields.autoTranslate_placeholderMode.label'), 'autoTranslate.concurrency': t('settings.fields.autoTranslate_concurrency.label'), 'autoTranslate.batchSize': t('settings.fields.autoTranslate_batchSize.label'), 'autoTranslate.progressInterval': t('settings.fields.autoTranslate_progressInterval.label'), 'autoTranslate.retryCount': t('settings.fields.autoTranslate_retryCount.label'), 'autoTranslate.retryDelay': t('settings.fields.autoTranslate_retryDelay.label'), 'autoTranslate.timeout': t('settings.fields.autoTranslate_timeout.label'), 'autoTranslate.dryRunFirst': t('settings.fields.autoTranslate_dryRunFirst.label'), 'autoTranslate.onlyMissingOrEnglish': t('settings.fields.autoTranslate_onlyMissingOrEnglish.label'), 'autoTranslate.reportStdout': t('settings.fields.autoTranslate_reportStdout.label'), 'autoTranslate.bom': t('settings.fields.autoTranslate_bom.label'), 'autoTranslate.protectionEnabled': t('settings.fields.autoTranslate_protectionEnabled.label'), 'autoTranslate.protectionFile': t('settings.fields.autoTranslate_protectionFile.label'), 'autoTranslate.promptProtectionSetup': t('settings.fields.autoTranslate_promptProtectionSetup.label'), 'autoTranslate.promptProtectionUpdate': t('settings.fields.autoTranslate_promptProtectionUpdate.label') }; await this.showSettingsCategory(autoTranslateSettings); } /** * Show advanced settings menu */ async showSecuritySettings() { // Refresh language from settings to ensure consistency if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } this.clearScreen(); this.showHeader(); console.log(`${colors.bright}${t('settings.security.title')}${colors.reset}\n`); const config = await this.adminAuth.loadConfig(); const pinSet = config && !!config.pinHash; const protectionEnabled = isAdminPinEnabled(); const pinStatus = pinSet ? `${colors.green}${t('settings.security.pinConfigured')}${colors.reset}` : `${colors.red}${t('settings.security.pinNotConfigured')}${colors.reset}`; console.log(`${t('settings.security.currentPin')}: ${pinStatus}`); console.log(`Protection Status: ${protectionEnabled ? `${colors.green}enabled${colors.reset}` : `${colors.red}disabled${colors.reset}`}\n`); const securitySettings = { 'security.adminPinEnabled': t('settings.fields.adminPinEnabled.label'), '_setupPin': 'Configure Admin PIN', 'security.sessionTimeout': 'Session Timeout (minutes)', 'security.maxFailedAttempts': 'Max Failed Attempts', 'security.lockoutDuration': 'Lockout Duration (minutes)', 'security.pinProtection.enabled': 'PIN Protection Master Switch', '_configurePinProtection': 'Configure PIN Protected Scripts' }; await this.showSettingsCategory(securitySettings); } async showAdvancedSettings() { // Refresh language from settings to ensure consistency if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } this.clearScreen(); this.showHeader(); console.log(`${colors.bright}${t('settings.categories.advancedSettings')}${colors.reset}\n`); const advancedSettings = { 'advanced.strictMode': t('settings.fields.strictMode.label'), 'advanced.enableAuditLog': t('settings.fields.enableAuditLog.label'), 'advanced.backupBeforeChanges': t('settings.fields.backupBeforeChanges.label'), 'reports.format': t('settings.fields.reports_format.label') }; await this.showSettingsCategory(advancedSettings); } /** * Show backup settings menu */ async showBackupSettings() { // Refresh language from settings to ensure consistency if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } this.clearScreen(); this.showHeader(); console.log(`${colors.bright}${t('settings.backup.title')}${colors.reset}\n`); console.log(`${t('settings.backup.description')}\n`); const backupSettings = { 'backup.enabled': t('settings.backup.enabled'), 'backup.enabled.help': t('settings.backup.enabledHelp'), 'backup.singleFileMode': t('settings.backup.singleFileMode'), 'backup.singleFileMode.help': t('settings.backup.singleFileModeHelp'), 'backup.singleBackupFile': t('settings.backup.singleBackupFile'), 'backup.singleBackupFile.help': t('settings.backup.singleBackupFileHelp'), 'backup.retentionDays': t('settings.backup.retentionDays'), 'backup.retentionDays.help': t('settings.backup.retentionDaysHelp'), 'backup.maxBackups': t('settings.backup.maxBackups'), 'backup.maxBackups.help': t('settings.backup.maxBackupsHelp'), 'backup.confirm': t('settings.backup.confirm'), 'backup.confirm.help': t('settings.backup.confirmHelp') }; await this.showSettingsCategory(backupSettings); } /** * Show settings category with edit options */ async showSettingsCategory(categorySettings) { // Refresh language from settings to ensure consistency if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } const keys = Object.keys(categorySettings); // Display current values for (let index = 0; index < keys.length; index++) { const key = keys[index]; let value = this.getNestedValue(this.settings, key); let displayValue = this.formatValue(value, key); // Special display for admin PIN protection and PIN setup if (key === 'security.adminPinEnabled') { const config = await this.adminAuth.loadConfig(); const pinSet = config && !!config.pinHash; const pinStatus = pinSet ? `${colors.green}PIN configured${colors.reset}` : `${colors.red}PIN not configured${colors.reset}`; displayValue = `${displayValue} - ${pinStatus}`; } else if (key === '_setupPin') { const config = await this.adminAuth.loadConfig(); const pinSet = config && !!config.pinHash; displayValue = pinSet ? `${colors.green}PIN configured${colors.reset}` : `${colors.red}PIN not configured${colors.reset}`; } console.log(` ${colors.cyan}${index + 1}${colors.reset}) ${categorySettings[key]}`); console.log(` ${colors.dim}${t('settings.current')}: ${colors.reset}${displayValue}`); } console.log(`\n ${colors.yellow}b${colors.reset}) ${t('settings.back')}`); // Add reset option for script directories const isScriptDirectory = Object.keys(categorySettings).some(key => key.startsWith('scriptDirectories.')); if (isScriptDirectory) { console.log(` ${colors.red}r${colors.reset}) ${t('settings.resetScriptDirectories')}`); } console.log(); const prompt = isScriptDirectory ? 'Select setting to edit (or b for back, r for reset): ' : 'Select setting to edit (or b for back): '; const choice = await this.prompt(prompt); if (choice.toLowerCase() === 'b') { return; } if (choice.toLowerCase() === 'r' && isScriptDirectory) { await this.resetScriptDirectories(); await this.showSettingsCategory(categorySettings); return; } const index = parseInt(choice) - 1; if (index >= 0 && index < keys.length) { const key = keys[index]; await this.editSetting(key, categorySettings[key]); await this.showSettingsCategory(categorySettings); } else { this.error(t('common.invalidOption')); await this.pause(); await this.showSettingsCategory(categorySettings); } } /** * Check if setting requires admin authentication */ requiresAdminAuth(key) { const adminProtectedSettings = [ 'security.adminPinEnabled', 'security.sessionTimeout', 'security.maxFailedAttempts', 'security.lockoutDuration', 'security.pinProtection.enabled', 'advanced.strictMode', 'debug.enabled', 'debug.verboseLogging', 'advanced.backupBeforeChanges' ]; return adminProtectedSettings.includes(key); } /** * Get helper text for a setting */ getHelperText(key) { // Handle backup settings with proper dot notation if (key.startsWith('backup.')) { // Check if key already has .help suffix if (key.endsWith('.help')) { return t(`settings.fields.${key}`) || t('settings.noHelp'); } return t(`settings.fields.${key}.help`) || t('settings.noHelp'); } const helperKey = key.replace(/\./g, '_'); return t(`settings.fields.${helperKey}.help`) || t('settings.noHelp'); } /** * Get valid options for a setting */ getValidOptions(key, schema) { if (schema && schema.enum) { return schema.enum; } const validOptions = { 'theme': ['light', 'dark', 'auto'], 'dateFormat': ['YYYY-MM-DD', 'DD/MM/YYYY', 'MM-DD-YYYY'], 'notifications.enabled': ['true', 'false'], 'advanced.strictMode': ['true', 'false'], 'debug.enabled': ['true', 'false'], 'debug.verboseLogging': ['true', 'false'], 'security.adminPinEnabled': ['true', 'false'], 'security.pinProtection.enabled': ['true', 'false'], 'advanced.backupBeforeChanges': ['true', 'false'], 'reports.format': ['markdown', 'json', 'text'], 'backup.enabled': ['true', 'false'], 'backup.singleFileMode': ['true', 'false'], 'autoTranslate.placeholderMode': ['preserve', 'skip', 'send'], 'autoTranslate.dryRunFirst': ['true', 'false'], 'autoTranslate.onlyMissingOrEnglish': ['true', 'false'], 'autoTranslate.reportStdout': ['true', 'false'], 'autoTranslate.bom': ['true', 'false'], 'autoTranslate.protectionEnabled': ['true', 'false'], 'autoTranslate.promptProtectionSetup': ['true', 'false'], 'autoTranslate.promptProtectionUpdate': ['true', 'false'] }; if (key === 'language') { return settingsManager.getAvailableLanguages().map(l => l.code); } return validOptions[key] || null; } /** * Validate input value */ validateInput(value, key, schema) { const validOptions = this.getValidOptions(key, schema); if (validOptions) { if (!validOptions.includes(value.toLowerCase())) { if (key === 'language') { return { valid: false, message: 'Language not installed. Reinstall package to use.' }; } return { valid: false, message: `Invalid option. Valid options: ${validOptions.join(', ')}` }; } } // Numeric validations with proper ranges const validations = { 'batchSize': { min: 1, max: 10000, type: 'int' }, 'concurrentFiles': { min: 1, max: 100, type: 'int' }, 'sizingThreshold': { min: 1, max: 20000, type: 'int', unit: 'KB' }, 'autoSave': { min: 0, max: 60, type: 'int', unit: 'minutes' }, 'sessionTimeout': { min: 0, max: 60, type: 'int', unit: 'minutes' }, 'maxFailedAttempts': { min: 1, max: 10, type: 'int' }, 'lockoutDuration': { min: 1, max: 60, type: 'int', unit: 'minutes' }, 'backupRetention': { min: 1, max: 30, type: 'int', unit: 'days' }, 'logRetention': { min: 1, max: 90, type: 'int', unit: 'days' }, 'retentionDays': { min: 1, max: 365, type: 'int', unit: 'days' }, 'maxBackups': { min: 1, max: 3, type: 'int' }, 'autoTranslate.concurrency': { min: 1, max: 100, type: 'int' }, 'autoTranslate.batchSize': { min: 1, max: 10000, type: 'int' }, 'autoTranslate.progressInterval': { min: 1, max: 10000, type: 'int' }, 'autoTranslate.retryCount': { min: 0, max: 10, type: 'int' }, 'autoTranslate.retryDelay': { min: 0, max: 30000, type: 'int', unit: 'ms' }, 'autoTranslate.timeout': { min: 1000, max: 120000, type: 'int', unit: 'ms' } }; for (const [field, rules] of Object.entries(validations)) { if (key.includes(field)) { const num = parseInt(value); if (isNaN(num) || num < rules.min || num > rules.max) { const unit = rules.unit ? ` ${rules.unit}` : ''; return { valid: false, message: `${field} must be between ${rules.min} and ${rules.max}${unit}.` }; } } } // String validations if (key.includes('path') || key.includes('directory')) { if (value.includes('..') || value.includes('\\') || value.includes('//')) { return { valid: false, message: 'Invalid path format. Use forward slashes and avoid relative paths.' }; } } return { valid: true }; } /** * Edit a specific setting */ async editSetting(key, label) { // Special handling for PIN setup if (key === '_setupPin') { await this.handlePinSetup(); return; } // Special handling for PIN protection configuration if (key === '_configurePinProtection') { await this.configurePinProtection(); return; } if (key === 'removeUiLanguages') { if (this.rl && this.rl.pause) this.rl.pause(); const LocaleOptimizer = require('../utils/locale-optimizer.js'); const optimizer = new LocaleOptimizer(); await optimizer.interactiveSelect(); // Re-initialize readline if it was closed by locale optimizer if (!this.rl || !this.rl.resume) { const { getGlobalReadline } = require('../utils/cli'); this.rl = getGlobalReadline(); } if (this.rl && typeof this.rl.resume === 'function') { this.rl.resume(); } if (typeof uiI18n.refreshAvailableLanguages === 'function') { uiI18n.refreshAvailableLanguages(); } return; } // Check if admin authentication is required if (this.requiresAdminAuth(key) && !this.adminAuthenticated) { // Check if admin PIN is actually enabled and configured const adminPinEnabled = isAdminPinEnabled(); const config = await this.adminAuth.loadConfig(); const pinConfigured = adminPinEnabled && config && config.enabled === true && !!config.pinHash; if (pinConfigured) { console.log(`\n${t('settings.admin.authRequired', { label: label })}`); const pin = await this.promptPin(t('adminCli.enterPin')); if (!pin) { console.log(t('settings.admin.accessDenied')); await this.pause(); return; } const authenticated = await this.adminAuth.verifyPin(pin); if (!authenticated) { console.log(t('settings.admin.accessDenied')); await this.pause(); return; } this.adminAuthenticated = true; } else { // PIN not enabled or not configured, allow access this.adminAuthenticated = true; } } const currentValue = this.getNestedValue(this.settings, key); const schema = this.getSettingSchema(key); console.log(`\n${colors.bright}${t('settings.editing')}: ${label}${colors.reset}`); // Show helper text const helperText = this.getHelperText(key); console.log(`${colors.dim}${helperText}${colors.reset}\n`); // Show current value with special handling for admin PIN if (key === 'security.adminPinEnabled') { const config = await this.adminAuth.loadConfig(); const pinSet = config && config.enabled === true && !!config.pinHash; const pinDisplay = pinSet ? '****' : '(not set)'; console.log(`${t('settings.current')}: ${this.formatValue(currentValue, key)} (${t('settings.pin')}: ${pinDisplay})`); } else { console.log(`${t('settings.current')}: ${this.formatValue(currentValue, key)}`); } // Show valid options const validOptions = this.getValidOptions(key, schema); if (validOptions) { console.log(`\n${colors.cyan}${t('settings.validOptions')}:${colors.reset}`); validOptions.forEach((option, index) => { const marker = option.toLowerCase() === String(currentValue).toLowerCase() ? ` ← ${t('settings.current')}` : ''; console.log(` ${index + 1}) ${option}${colors.dim}${marker}${colors.reset}`); }); } console.log(); const newValue = await this.prompt(t('settings.enterNewValue')); if (newValue.trim() === '') { return; } // Handle "default" keyword for script directory settings if (newValue.trim().toLowerCase() === 'default') { if (key.startsWith('scriptDirectories.')) { this.setNestedValue(this.settings, key, null); this.modified = true; this.success(`${label} reset to system default.`); await this.pause(); return; } else { this.error('"default" keyword is only supported for script directory settings.'); await this.pause(); return; } } // Validate input const validation = this.validateInput(newValue.trim(), key, schema); if (!validation.valid) { this.error(validation.message); await this.pause(); return; } // Convert the value const convertedValue = this.convertValue(newValue.trim(), schema, key); if (convertedValue === null) { this.error(t('settings.invalidValueFormat')); await this.pause(); return; } // Special handling for admin PIN protection toggle if (key === 'security.adminPinEnabled') { const config = await this.adminAuth.loadConfig(); const pinSet = config && config.pinHash; if (convertedValue === true) { // Enable protection if (!pinSet) { // No PIN exists, need to set one up console.log('\nšŸ” Admin PIN Protection Setup'); console.log('You must set a PIN to enable protection.\n'); const pin = await this.promptPin('Enter new admin PIN (4-6 digits): '); if (pin) { const confirmPin = await this.promptPin('Confirm PIN: '); if (pin === confirmPin) { const success = await this.adminAuth.setupPin(pin); if (success) { this.setNestedValue(this.settings, key, true); this.modified = true; try { await this.saveSettings(); this.success('Admin PIN protection enabled successfully!'); } catch (error) { this.error(`Failed to save settings: ${error.message}`); } } else { this.error('Failed to set admin PIN.'); return; } } else { this.error('PINs do not match.'); return; } } else { this.warning('PIN setup cancelled. Protection not enabled.'); return; } } else { // PIN already exists, just enable protection const success = await this.adminAuth.enablePinProtection(); if (success) { this.setNestedValue(this.settings, key, true); this.modified = true; try { await this.saveSettings(); this.success('Admin PIN protection enabled!'); } catch (error) { this.error(`Failed to save settings: ${error.message}`); } } else { this.error('Failed to enable PIN protection.'); return; } } } else { // Disable protection and remove PIN console.log('\nāš ļø This will disable admin PIN protection and remove the configured PIN.'); if (pinSet) { // PIN exists, need to verify before disabling const pin = await this.promptPin('Enter current PIN to confirm: '); if (pin && await this.adminAuth.verifyPin(pin)) { const success = await this.adminAuth.disableAuth(); if (success) { this.setNestedValue(this.settings, key, false); this.modified = true; this.adminAuthenticated = false; try { await this.saveSettings(); this.success('Admin PIN protection disabled and PIN removed!'); } catch (error) { this.error(`Failed to save settings: ${error.message}`); } } else { this.error('Failed to disable PIN protection.'); return; } } else if (pin) { this.error('Invalid PIN. Protection not disabled.'); return; } else { this.warning('Operation cancelled. Protection remains enabled.'); return; } } else { // No PIN set, just disable protection await this.adminAuth.disableAuth(); this.setNestedValue(this.settings, key, false); this.modified = true; this.adminAuthenticated = false; try { await this.saveSettings(); this.success('Admin PIN protection disabled!'); } catch (error) { this.error(`Failed to save settings: ${error.message}`); } } } return; } // Update the setting for all other cases let finalValue = convertedValue; if (typeof finalValue === 'string') { const lowerKey = key.toLowerCase(); if (lowerKey.includes('dir') || lowerKey.includes('path') || lowerKey.includes('root') || key === 'autoTranslate.protectionFile') { finalValue = finalValue.replace(/^([/\\])/, './'); finalValue = configManager.toRelative(path.resolve(finalValue)); } } this.setNestedValue(this.settings, key, finalValue); this.modified = true; // Save settings immediately after each change try { await this.saveSettings(); } catch (error) { this.error(`Failed to save settings: ${error.message}`); } // Special handling for language changes if (key === 'language') { // Refresh UI language immediately if (typeof uiI18n.refreshLanguageFromSettings === 'function') { uiI18n.refreshLanguageFromSettings(); } // Refresh i18n-helper translations const { refreshLanguageFromSettings } = require('../utils/i18n-helper'); refreshLanguageFromSettings(); // Force reload settings to ensure consistency this.settings = configManager.getConfig(); } } /** * Prompt for PIN input with masking */ async promptPin(prompt) { const input = await cliHelper.prompt(prompt, true); const pin = input.trim(); if (/^\d{4,6}$/.test(pin)) { return pin; } else { console.log('āŒ PIN must be 4-6 digits.'); return null; } } /** * Handle PIN setup/change */ async handlePinSetup() { this.clearScreen(); this.showHeader(); console.log(`${colors.bright}${t('settings.admin.pinSetupTitle')}${colors.reset}\n`); const config = await this.adminAuth.loadConfig(); const pinSet = config && config.enabled === true && !!config.pinHash; if (pinSet) { console.log(t('settings.admin.pinConfigured')); console.log('\n' + t('settings.admin.options')); console.log(' ' + t('settings.admin.changePin')); console.log(' ' + t('settings.admin.removePin')); console.log(' ' + t('settings.admin.cancel')); console.log(); const choice = await this.prompt(t('settings.admin.selectOption')); switch (choice) { case '1': console.log(t('settings.admin.verifyCurrentPin')); const currentPin = await this.promptPin('Enter current PIN: '); if (currentPin && await this.adminAuth.verifyPin(currentPin)) { const newPin = await this.promptPin('Enter new PIN: '); if (newPin) { const confirmPin = await this.promptPin('Confirm new PIN: '); if (newPin === confirmPin) { const success = await this.adminAuth.setupPin(newPin); if (success) { this.success('Admin PIN updated successfully!'); // Save settings immediately try { await this.saveSettings(); } catch (error) { this.error(`Failed to save settings: ${error.message}`); } } else { this.error('Failed to update admin PIN.'); } } else { this.error('PINs do not match.'); } } } else { this.error('Invalid current PIN.'); } break; case '2': console.log(t('settings.admin.verifyToRemove')); const removePin = await this.promptPin('Enter PIN to confirm removal: '); if (removePin && await this.adminAuth.verifyPin(removePin)) { const success = await this.adminAuth.disableAuth(); if (success) { this.success('Admin PIN protection removed.'); // Save settings immediately try { await this.saveSettings(); } catch (error) { this.error(`Failed to save settings: ${error.message}`); } } else { this.error('Failed to remove PIN protection.'); } } else { this.error('Invalid PIN.'); } break; case '3': console.log(t('settings.admin.operationCancelled')); break; default: this.error(t('common.invalidOption')); } } else { console.log(t('settings.admin.noPinConfigured')); console.log('\n' + t('settings.admin.pinBenefits')); console.log(' ' + t('settings.admin.benefitSecurity')); console.log(' ' + t('settings.admin.benefitAdvanced')); console.log(' ' + t('settings.admin.benefitDebug')); console.log(' ' + t('settings.admin.benefitReset')); console.log(); const response = await this.prompt(t('settings.admin.setupPinPrompt')); if (response.toLowerCase() === 'y' || response.toLowerCase() === 'yes') { const pin = await this.promptPin('Enter new admin PIN (4-6 digits): '); if (pin) { const confirmPin = await this.promptPin('Confirm PIN: '); if (pin === confirmPin) { const success = await this.adminAuth.setupPin(pin); if (success) { this.success('Admin PIN configured successfully!'); // Enable admin PIN protection in settings this.setNestedValue(this.settings, 'security.adminPinEnabled', true); this.modified = true; // Save settings immediately try { await this.saveSettings(); } catch (error) { this.error(`Failed to save settings: ${error.message}`); } } else { this.error('Failed to configure admin PIN.'); } } else { this.error('PINs do not match.'); } } } else { console.log(t('settings.admin.setupCancelled')); } } await this.pause(); } /** * Configure PIN protection for individual scripts */ async configurePinProtection() { this.clearScreen(); this.showHeader(); console.log(`${colors.bright}Configure PIN Protection${colors.reset}\n`); // Check if admin PIN is configured const adminPinConfigured = await this.adminAuth.isPinConfigured(); if (!adminPinConfigured) { console.log(`${colors.red}Admin PIN is not configured.${colors.reset}`); console.log(`${colors.yellow}Please configure an Admin PIN first in Security Settings.${colors.reset}\n`); const proceed = await this.prompt('Press Enter to continue...'); return; } // Check if PIN protection is enabled globally if (!isAdminPinEnabled()) { console.log(`${colors.red}PIN Protection is currently disabled.${colors.reset}`); console.log(`${colors.yellow}Please enable PIN Protection in Security Settings.${colors.reset}\n`); const proceed = await this.prompt('Press Enter to continue...'); return; } const pinProtection = this.getNestedValue(this.settings, 'security.pinProtection') || {}; const protectedScrip