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
JavaScript
/**
* 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