UNPKG

i18ntk

Version:

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

1,136 lines (995 loc) 68.4 kB
#!/usr/bin/env node /** * I18NTK MANAGEMENT TOOLKIT - MAIN MANAGER * * This is the main entry point for all i18n operations. * It provides an interactive interface to manage translations. * * Usage: * npm run i18ntk:manage * npm run i18ntk:manage -- --command=init * npm run i18ntk:manage -- --command=analyze * npm run i18ntk:manage -- --command=validate * npm run i18ntk:manage -- --help */ const path = require('path'); const fs = require('fs'); const AdminAuth = require('../../utils/admin-auth'); const SecurityUtils = require('../../utils/security'); const SettingsManager = require('../../settings/settings-manager'); const { validateSourceDir } = require('../../utils/config-helper'); const { checkInitialized } = require('../../utils/init-helper'); const { showFrameworkWarningOnce } = require('../../utils/cli-helper'); const { createPrompt, isInteractive } = require('../../utils/prompt-helper'); const { loadTranslations, t, refreshLanguageFromSettings} = require('../../utils/i18n-helper'); const { formatLanguagePrompt } = require('../../utils/language-menu'); const cliHelper = require('../../utils/cli-helper'); const { printUpgradeWarningIfOutdated } = require('../../utils/npm-version-warning'); const { blue } = require('../../utils/colors-new'); const { loadConfig, saveConfig, ensureConfigDefaults } = require('../../utils/config'); const { buildMainMenuLines } = require('../../utils/menu-layout'); const SettingsCLI = require('../../settings/settings-cli'); const pkg = require('../../package.json'); const SetupEnforcer = require('../../utils/setup-enforcer'); const CommandRouter = require('./commands/CommandRouter'); const { detectProjectFramework } = require('../../utils/framework-detector'); const configManager = new SettingsManager(); // Import services to replace circular dependencies const ConfigurationService = require('./services/ConfigurationService'); const SummaryService = require('./services/SummaryService'); // Preload translations early to avoid missing key warnings loadTranslations(); class I18nManager { constructor(config = {}) { this.config = config; this.rl = null; this.isReadlineClosed = false; this.isAuthenticated = false; this.ui = null; this.adminAuth = new AdminAuth(); this.commandRouter = null; // Initialize services this.configurationService = new ConfigurationService(config); this.summaryService = new SummaryService(config); // No longer create readline interface here - use CLI helpers } initializeReadline() { // Use centralized CLI helper instead of direct readline this.rl = null; this.isReadlineClosed = false; } // Initialize configuration using unified system async initialize(options = {}) { try { // Parse args here for other initialization needs (but language is already loaded) const args = this.parseArgs(); const interactive = options.interactive ?? isInteractive({ noPrompt: options.noPrompt || args.noPrompt }); if (args.help) { this.showHelp(); process.exit(0); } // Load translations for the UI language const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); const uiLanguage = args.uiLanguage || settings.uiLanguage || settings.language || this.config.uiLanguage || 'en'; loadTranslations(uiLanguage); // Validate source directory exists try { validateSourceDir(this.config.sourceDir, 'i18ntk-manage'); } catch (err) { console.log(t('init.requiredTitle')); console.log(t('init.requiredBody')); if (interactive) { const answer = await cliHelper.prompt(t('init.promptRunNow')); if (answer.trim().toLowerCase() === 'y') { // Note: Initialization should be handled by the calling code // to avoid circular dependencies console.log('Please run initialization manually or use the init command'); } else { throw err; } } else { // Non-interactive: surface the original error to avoid hanging throw err; } } } catch (error) { throw error; } } // Auto-detect i18n directory from common locations only if not configured in settings detectI18nDirectory() { const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); const projectRoot = path.resolve(settings.projectRoot || this.config.projectRoot || '.'); // Use per-script directory configuration if available, fallback to global sourceDir const sourceDir = settings.scriptDirectories?.manage || settings.sourceDir; if (sourceDir) { this.config.sourceDir = path.resolve(projectRoot, sourceDir); return; } // Define possible i18n paths for auto-detection const possibleI18nPaths = [ './locales', './src/locales', './src/i18n', './src/i18n/locales', './app/locales', './app/i18n', './public/locales', './assets/locales', './translations', './lang' ]; // Only auto-detect if no settings are configured for (const possiblePath of possibleI18nPaths) { const resolvedPath = path.resolve(projectRoot, possiblePath); if (SecurityUtils.safeExistsSync(resolvedPath)) { // Check if it contains language directories try { const items = fs.readdirSync(resolvedPath); const hasLanguageDirs = items.some(item => { const itemPath = path.join(resolvedPath, item); return fs.statSync(itemPath).isDirectory() && ['en', 'de', 'es', 'fr', 'it', 'pt', 'nl', 'pl', 'sv', 'uk', 'cs', 'tr', 'ru', 'ja', 'ko', 'zh', 'ar', 'hi', 'th', 'vi', 'he', 'el', 'hu'].includes(item); }); if (hasLanguageDirs) { this.config.sourceDir = possiblePath; break; } } catch (error) { // Continue checking other paths } } } } // Check if i18n framework is installed - configuration-based check without prompts async checkI18nDependencies() { const markInternalFrameworkDetected = () => { const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); cfg.framework = cfg.framework || {}; cfg.framework.detected = true; cfg.framework.preference = cfg.framework.preference || 'i18ntk'; const existing = Array.isArray(cfg.framework.installed) ? cfg.framework.installed : []; if (!existing.includes('i18ntk')) { cfg.framework.installed = [...existing, 'i18ntk']; } if (configManager.saveSettings) { configManager.saveSettings(cfg); } else if (configManager.saveConfig) { configManager.saveConfig(cfg); } }; try { const initStatus = await checkInitialized(); if (initStatus?.initialized || initStatus?.config?.setup?.completed) { markInternalFrameworkDetected(); return true; } } catch (_) { // Continue with package.json detection. } const packageJsonPath = path.resolve('./package.json'); if (!SecurityUtils.safeExistsSync(packageJsonPath)) { console.log(this.ui ? this.ui.t('errors.noPackageJson') : 'No package.json found'); return false; // Treat as no framework detected } try { const packageJson = JSON.parse(SecurityUtils.safeReadFileSync(packageJsonPath, path.dirname(packageJsonPath), 'utf8')); // Include peerDependencies in the check const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies, ...packageJson.peerDependencies }; const i18nFrameworks = [ 'react-i18next', 'vue-i18n', 'i18next', 'next-i18next', 'svelte-i18n', '@nuxtjs/i18n', 'next-intl', 'remix-i18next', 'i18next-remix', 'gatsby-plugin-react-i18next', 'gatsby-plugin-intl', 'sveltekit-i18n', 'astro-i18next', '@astrojs/i18n', 'qwik-speak', 'qwik-i18n', '@solid-primitives/i18n', 'ember-intl', 'react-native-localize', 'expo-localization', '@ngx-translate/core', '@ionic/angular', 'formatjs', '@lingui/core', 'i18ntk-runtime', 'i18ntk/runtime', 'angular-i18n', '@angular/localize', 'i18ntk' ]; const installedFrameworks = i18nFrameworks.filter(framework => dependencies[framework]); if (installedFrameworks.length > 0) { if (this.ui && this.ui.t) { console.log(this.ui.t('init.detectedFrameworks', { frameworks: installedFrameworks.join(', ') })); } else { console.log(`Detected frameworks: ${installedFrameworks.join(', ')}`); } const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); cfg.framework = cfg.framework || {}; cfg.framework.detected = true; cfg.framework.installed = installedFrameworks; if (configManager.saveSettings) { configManager.saveSettings(cfg); } else if (configManager.saveConfig) { configManager.saveConfig(cfg); } return true; } else { const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); if (cfg.framework) { cfg.framework.detected = false; if (configManager.saveSettings) { configManager.saveSettings(cfg); } else if (configManager.saveConfig) { configManager.saveConfig(cfg); } } // Always signal that frameworks were not detected return false; } } catch (error) { console.log(t('init.errors.packageJsonRead')); return false; // Treat as no framework detected on error } } // Parse command line arguments parseArgs() { const args = process.argv.slice(2); const parsed = {}; args.forEach(arg => { if (arg.startsWith('--')) { const [key, value] = arg.substring(2).split('='); const sanitizedKey = key?.trim(); const sanitizedValue = value !== undefined ? value.trim() : true; switch (sanitizedKey) { case 'source-dir': case 'code-dir': case 'source-code-dir': parsed.sourceDir = sanitizedValue; break; case 'locales-dir': case 'i18n-dir': parsed.i18nDir = sanitizedValue; break; case 'output-dir': parsed.outputDir = sanitizedValue; break; case 'source-language': case 'source-locale': parsed.sourceLanguage = sanitizedValue; break; case 'ui-language': parsed.uiLanguage = sanitizedValue; break; case 'no-prompt': parsed.noPrompt = true; break; case 'admin-pin': parsed.adminPin = sanitizedValue || ''; break; case 'help': case 'h': parsed.help = true; break; default: // Handle language shorthand flags like --de, --fr if (['en', 'de', 'es', 'fr', 'it', 'pt', 'nl', 'pl', 'sv', 'uk', 'cs', 'tr', 'ru', 'ja', 'ko', 'zh', 'ar', 'hi', 'th', 'vi', 'he', 'el', 'hu'].includes(sanitizedKey)) { parsed.uiLanguage = sanitizedKey; } break; } } }); return parsed; } // Add this run method after the checkI18nDependencies method async run() { // Parse args early so we can short-circuit help/version flows before setup const args = this.parseArgs(); const rawArgs = process.argv.slice(2); const directCommands = [ 'init', 'analyze', 'validate', 'usage', 'scanner', 'sizing', 'complete', 'fix', 'summary', 'debug', 'workflow', 'report' ]; const commandFlagArg = rawArgs.find(arg => arg.startsWith('--command=')); const requestedCommand = commandFlagArg ? commandFlagArg.split('=')[1] : (rawArgs.length > 0 && directCommands.includes(rawArgs[0]) ? rawArgs[0] : null); const shouldSkipInitCheck = requestedCommand === 'init' || requestedCommand === 'report'; // Show help immediately without any setup/auth (useful for CI/uninitialized projects) if (args.help) { this.showHelp(); return; } if (requestedCommand === 'report') { const ReportCommand = require('./commands/ReportCommand'); const command = new ReportCommand(this.config, this.ui); await command.execute(args); this.safeClose(); return; } // Warn users when their installed CLI version is behind npm latest. try { await printUpgradeWarningIfOutdated({ packageName: pkg.name, currentVersion: pkg.version }); } catch { // Keep startup resilient if registry access is unavailable. } let startupTimeout = setTimeout(() => { console.error('❌ CLI startup timeout - something is hanging'); process.exit(1); }, 10000); // 10 second timeout const clearStartupTimeout = () => { if (startupTimeout) { clearTimeout(startupTimeout); startupTimeout = null; } }; let prompt; try { // Allow `init` to run even when setup is not complete. if (!shouldSkipInitCheck) { await SetupEnforcer.checkSetupCompleteAsync(); } prompt = createPrompt({ noPrompt: args.noPrompt }); const interactive = isInteractive({ noPrompt: args.noPrompt }); // Load settings and UI language const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); const uiLanguage = args.uiLanguage || settings.uiLanguage || settings.language || this.config.uiLanguage || 'en'; loadTranslations(uiLanguage); // Initialize CommandRouter this.commandRouter = new CommandRouter(this.config, this.ui, this.adminAuth); if (args.help) { this.showHelp(); return; } let cfgAfterInitCheck = {}; if (interactive && !shouldSkipInitCheck) { cfgAfterInitCheck = await this.ensureInitializedOrExit(prompt); const frameworksDetected = await this.checkI18nDependencies(); if (!frameworksDetected) { await this.maybePromptFramework(prompt, cfgAfterInitCheck, pkg.version); } } this.config = { ...this.config, ...cfgAfterInitCheck }; await this.initialize({ interactive, noPrompt: args.noPrompt }); clearStartupTimeout(); let commandToExecute = requestedCommand; // Handle help immediately without dependency checks if (args.help) { this.showHelp(); this.safeClose(); process.exit(0); } // Handle debug flag if (args.debug) { // Enable debug mode for this session console.log(blue('Debug mode enabled')); } if (commandToExecute) { await this.executeCommand(commandToExecute, args); this.safeClose(); return; } // If no command provided and --no-prompt is set, exit gracefully if (args.noPrompt) { this.safeClose(); process.exit(0); } // Framework detection is now handled by maybePromptFramework above // Skip the redundant checkI18nDependencies prompt // Interactive mode - showInteractiveMenu will handle the title await this.showInteractiveMenu(); } catch (error) { if (this.ui && this.ui.t) { console.error(t('common.genericError', { error: error.message })); } else { console.error(`Error: ${error.message}`); } process.exit(1); } finally { clearStartupTimeout(); if (prompt && typeof prompt.close === 'function') { prompt.close(); } this.safeClose(); } } showHelp() { const localT = this.ui && this.ui.t ? this.ui.t.bind(this.ui) : (key) => { // Fallback help text when UI is not initialized const helpTexts = { 'help.usage': 'Usage: npm run i18ntk [command] [options]', 'help.interactiveMode': 'Run without arguments for interactive mode', 'help.initProject': ' init - Initialize i18n project structure', 'help.analyzeTranslations': ' analyze - Analyze translation files', 'help.validateTranslations': ' validate - Validate translations for errors', 'help.checkUsage': ' usage - Check translation usage in code', 'help.showHelp': ' help - Show this help message', 'help.availableCommands': '\nAvailable commands:', 'help.initCommand': ' --command=init Initialize i18n project', 'help.analyzeCommand': ' --command=analyze Analyze translations', 'help.validateCommand': ' --command=validate Validate translations', 'help.usageCommand': ' --command=usage Check translation usage', 'help.sizingCommand': ' --command=sizing Analyze translation sizing', 'help.completeCommand': ' --command=complete Run complete analysis', 'help.summaryCommand': ' --command=summary Generate summary report', 'help.debugCommand': ' --command=debug Run debug utilities', 'help.scannerCommand': ' --command=scanner Scan for translation keys' }; return helpTexts[key] || key; }; console.log(t('help.usage')); console.log(t('help.interactiveMode')); console.log(t('help.initProject')); console.log(t('help.analyzeTranslations')); console.log(t('help.validateTranslations')); console.log(t('help.checkUsage')); console.log(t('help.showHelp')); console.log(t('help.availableCommands')); console.log(t('help.initCommand')); console.log(t('help.analyzeCommand')); console.log(t('help.validateCommand')); console.log(t('help.usageCommand')); console.log(t('help.sizingCommand')); console.log(t('help.completeCommand')); console.log(t('help.summaryCommand')); console.log(t('help.debugCommand')); console.log(t('help.scannerCommand')); // Ensure proper exit for direct command execution if (process.argv.includes('--help') || process.argv.includes('-h')) { this.safeClose(); process.exit(0); } } /** * Execute a command using the new CommandRouter system */ async executeCommand(command, options = {}) { if (!this.commandRouter) { throw new Error('CommandRouter not initialized'); } // Set runtime dependencies for the command router this.commandRouter.setRuntimeDependencies( this.prompt.bind(this), this.isNonInteractiveMode(), this.safeClose.bind(this) ); return await this.commandRouter.executeCommand(command, options); } // ... existing code for ensureInitializedOrExit, maybePromptFramework, showInteractiveMenu, etc. ... async ensureInitializedOrExit(prompt) { const { initialized, config } = await checkInitialized(); if (!initialized) { console.log('\nThis project is not yet initialized with i18ntk.'); const shouldInitialize = await cliHelper.confirm('Would you like to initialize it now?'); if (!shouldInitialize) { console.log('Exiting. Please initialize the project first.'); process.exit(1); } // The initialization will be handled by the init command return config; } // Check if we need to prompt for framework detection const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); // Ensure framework configuration exists with all required fields if (!settings.framework) { settings.framework = { detected: false, preference: null, prompt: 'always', lastPromptedVersion: null, installed: [], version: '1.0' // Schema version for future compatibility }; } const setupCompleted = Boolean(config?.setup?.completed || settings?.setup?.completed); if (setupCompleted) { settings.framework.detected = true; settings.framework.preference = settings.framework.preference || 'i18ntk'; settings.framework.installed = Array.isArray(settings.framework.installed) ? Array.from(new Set([...settings.framework.installed, 'i18ntk'])) : ['i18ntk']; settings.framework.lastDetected = new Date().toISOString(); if (configManager.saveSettings) { await configManager.saveSettings(settings); } else if (configManager.saveConfig) { await configManager.saveConfig(settings); } return { ...config, ...settings }; } // Check if we need to prompt for framework detection if (!settings.framework.detected && settings.framework.prompt !== 'suppress' && settings.framework.lastPromptedVersion !== pkg.version) { console.log('\nWe noticed you haven\'t set up an i18n framework yet.'); console.log('Would you like to detect your i18n framework automatically?'); console.log('1. Detect automatically'); console.log('2. I\'ll set it up manually'); console.log('3. Don\'t show this again'); const answer = await prompt.question('\nSelect an option (1-3): '); const choice = answer.trim(); let action; if (choice === '1') action = 'detect'; else if (choice === '2') action = 'manual'; else if (choice === '3') action = 'dont-show'; else action = 'manual'; // default fallback if (action === 'dont-show') { // Update settings to suppress future prompts for this version settings.framework.prompt = 'suppress'; settings.framework.lastPromptedVersion = pkg.version; if (configManager.saveSettings) { await configManager.saveSettings(settings); } else if (configManager.saveConfig) { await configManager.saveConfig(settings); } console.log('Framework detection prompt will be suppressed for this version.'); } else if (action === 'detect') { // Run framework detection const { detectedLanguage, detectedFramework } = await this.detectEnvironmentAndFramework(); if (detectedFramework && detectedFramework !== 'generic') { console.log(`\nDetected framework: ${detectedFramework}`); // Update settings with detected framework settings.framework.detected = true; settings.framework.preference = detectedFramework; settings.framework.lastDetected = new Date().toISOString(); if (configManager.saveSettings) { await configManager.saveSettings(settings); } else if (configManager.saveConfig) { await configManager.saveConfig(settings); } console.log(`Framework set to: ${detectedFramework}`); } else { console.log('\nCould not detect a specific i18n framework.'); console.log('Please set up your i18n framework manually.'); } } } return { ...config, ...settings }; } async detectEnvironmentAndFramework() { // Defensive check to ensure SecurityUtils is available if (!SecurityUtils) { throw new Error('SecurityUtils is not available. This may indicate a module loading issue.'); } const packageJsonPath = path.join(process.cwd(), 'package.json'); const pyprojectPath = path.join(process.cwd(), 'pyproject.toml'); const requirementsPath = path.join(process.cwd(), 'requirements.txt'); const goModPath = path.join(process.cwd(), 'go.mod'); const pomPath = path.join(process.cwd(), 'pom.xml'); const composerPath = path.join(process.cwd(), 'composer.json'); let detectedLanguage = 'generic'; let detectedFramework = 'generic'; try { const initStatus = await checkInitialized(); if (initStatus?.initialized || initStatus?.config?.setup?.completed) { return { detectedLanguage: 'javascript', detectedFramework: 'i18ntk' }; } } catch (_) { // Continue with filesystem/package detection. } if (SecurityUtils.safeExistsSync(packageJsonPath)) { detectedLanguage = 'javascript'; try { const packageJson = JSON.parse(SecurityUtils.safeReadFileSync(packageJsonPath, path.dirname(packageJsonPath), 'utf8')); const deps = { ...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {}), ...(packageJson.peerDependencies || {}) }; // Check for i18ntk-runtime first (check both package names) const hasI18nTkRuntime = deps['i18ntk-runtime'] || deps['i18ntk/runtime']; const hasI18nTkToolkit = Boolean(deps['i18ntk']); if (hasI18nTkToolkit) { detectedFramework = 'i18ntk'; } // Check for common i18n patterns in source code if not found in package.json if (!hasI18nTkRuntime && detectedFramework !== 'i18ntk') { const i18nPatterns = [ /i18n\.t\(['\"`]/, /useI18n\(/, /from ['\"]i18ntk[\/\\]runtime['\"]/, /require\(['\"]i18ntk[\/\\]runtime['\"]\)/ ]; const sourceFiles = await this.customGlob(['src/**/*.{js,jsx,ts,tsx}'], { cwd: process.cwd(), ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] }); for (const file of sourceFiles) { try { const content = await fs.promises.readFile(path.join(process.cwd(), file), 'utf8'); if (i18nPatterns.some(pattern => pattern.test(content))) { detectedFramework = 'i18ntk-runtime'; break; } } catch (e) { // Skip files we can't read continue; } } } else if (hasI18nTkRuntime) { detectedFramework = 'i18ntk-runtime'; } // Use centralized framework detector if (detectedFramework !== 'i18ntk-runtime') { const centralDetected = detectProjectFramework(process.cwd()); detectedFramework = (centralDetected && centralDetected !== 'vanilla') ? centralDetected : 'generic'; } } catch (error) { detectedFramework = 'generic'; } } else if (SecurityUtils.safeExistsSync(pyprojectPath) || SecurityUtils.safeExistsSync(requirementsPath)) { detectedLanguage = 'python'; try { if (SecurityUtils.safeExistsSync(requirementsPath)) { const requirements = SecurityUtils.safeReadFileSync(requirementsPath, path.dirname(requirementsPath), 'utf8'); if (requirements.includes('django')) detectedFramework = 'django'; else if (requirements.includes('flask')) detectedFramework = 'flask'; else if (requirements.includes('fastapi')) detectedFramework = 'fastapi'; else detectedFramework = 'generic'; } } catch (error) { detectedFramework = 'generic'; } } else if (SecurityUtils.safeExistsSync(goModPath)) { detectedLanguage = 'go'; detectedFramework = 'generic'; } else if (SecurityUtils.safeExistsSync(pomPath)) { detectedLanguage = 'java'; try { const pomContent = SecurityUtils.safeReadFileSync(pomPath, path.dirname(pomPath), 'utf8'); if (pomContent.includes('spring-boot')) detectedFramework = 'spring-boot'; else if (pomContent.includes('spring')) detectedFramework = 'spring'; else if (pomContent.includes('quarkus')) detectedFramework = 'quarkus'; else detectedFramework = 'generic'; } catch (error) { detectedFramework = 'generic'; } } else if (SecurityUtils.safeExistsSync(composerPath)) { detectedLanguage = 'php'; try { const composer = JSON.parse(SecurityUtils.safeReadFileSync(composerPath, path.dirname(composerPath), 'utf8')); const deps = composer.require || {}; if (deps['laravel/framework']) detectedFramework = 'laravel'; else if (deps['symfony/framework-bundle']) detectedFramework = 'symfony'; else if (deps['wordpress']) detectedFramework = 'wordpress'; else detectedFramework = 'generic'; } catch (error) { detectedFramework = 'generic'; } } return { detectedLanguage, detectedFramework }; } async customGlob(patterns, options = {}) { const cwd = options.cwd || process.cwd(); const ignorePatterns = options.ignore || []; function matchesPattern(filename, pattern) { // Simple pattern matching for **/*.{js,jsx,ts,tsx} style patterns if (pattern.includes('**/*')) { const extensionPart = pattern.split('*.')[1]; if (extensionPart) { const extensions = extensionPart.replace('{', '').replace('}', '').split(','); return extensions.some(ext => filename.endsWith('.' + ext.trim())); } } return filename.includes(pattern.replace('**/', '')); } function shouldIgnore(filePath) { return ignorePatterns.some(pattern => { if (pattern.includes('**/')) { const patternEnd = pattern.replace('**/', ''); return filePath.includes('/' + patternEnd) || filePath.includes('\\' + patternEnd); } return filePath.includes(pattern); }); } function findFiles(dir, results = []) { try { const items = SecurityUtils.safeReaddirSync(dir, cwd, { withFileTypes: true }); if (!items) return results; for (const item of items) { const fullPath = path.join(dir, item.name); const relativePath = path.relative(cwd, fullPath); if (shouldIgnore(relativePath)) { continue; } try { if (item.isDirectory()) { findFiles(fullPath, results); } else if (item.isFile()) { // Check if file matches any of our patterns for (const pattern of patterns) { if (matchesPattern(item.name, pattern)) { results.push(relativePath); break; } } } } catch (error) { // Skip files we can't access continue; } } } catch (error) { // Skip directories we can't access } return results; } return findFiles(cwd); } async maybePromptFramework(prompt, cfg, currentVersion) { // Load current settings to check framework configuration let settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); // Ensure framework configuration exists with all required fields if (!settings.framework) { settings.framework = { detected: false, preference: null, prompt: 'always', lastPromptedVersion: null, installed: [], version: '1.0' // Schema version for future compatibility }; // Save the updated settings if (configManager.saveSettings) { await configManager.saveSettings(settings); } else if (configManager.saveConfig) { await configManager.saveConfig(settings); } } // Reload settings to ensure we have the latest framework detection results const freshSettings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); if (freshSettings.framework) { settings.framework = { ...settings.framework, ...freshSettings.framework }; } // Check if framework is already detected or preference is explicitly set to none if (settings.framework.detected || settings.framework.preference === 'none') { return cfg; } // Check if DNR (Do Not Remind) is active for this version if (settings.framework.prompt === 'suppress' && settings.framework.lastPromptedVersion === currentVersion) { return cfg; } // Reset DNR if version changed if (settings.framework.prompt === 'suppress' && settings.framework.lastPromptedVersion !== currentVersion) { settings.framework.prompt = 'always'; settings.framework.lastPromptedVersion = null; // Save the updated settings if (configManager.saveSettings) { await configManager.saveSettings(settings); } else if (configManager.saveConfig) { await configManager.saveConfig(settings); } } // This function is now handled by ensureInitializedOrExit for better flow control return cfg; } async showInteractiveMenu() { // Check if we're in non-interactive mode (like echo 0 | node script) if (this.isNonInteractiveMode()) { console.log(buildMainMenuLines(t).join('\n')); console.log('\n' + t('menu.nonInteractiveModeWarning')); console.log(t('menu.useDirectExecution')); console.log(t('menu.useHelpForCommands')); this.safeClose(); process.exit(0); return; } console.log(buildMainMenuLines(t).join('\n')); const choice = await this.prompt('\n' + t('menu.selectOptionPrompt')); switch (choice.trim()) { case '1': await this.executeCommand('init', {fromMenu: true}); await this.showInteractiveMenu(); return; case '2': await this.executeCommand('analyze', {fromMenu: true}); await this.showInteractiveMenu(); return; case '3': await this.executeCommand('validate', {fromMenu: true}); await this.showInteractiveMenu(); return; case '4': await this.executeCommand('usage', {fromMenu: true}); await this.showInteractiveMenu(); return; case '5': await this.executeCommand('complete', {fromMenu: true}); await this.showInteractiveMenu(); return; case '6': await this.executeCommand('sizing', {fromMenu: true}); await this.showInteractiveMenu(); return; case '7': await this.executeCommand('fix', {fromMenu: true}); await this.showInteractiveMenu(); return; case '8': // Check for PIN protection const authRequired = await this.adminAuth.isAuthRequiredForScript('summaryReports'); if (authRequired) { console.log(`\n${t('adminCli.protectedAccess')}`); const pin = await cliHelper.promptPin(t('adminCli.enterPin') + ': '); const isValid = await this.adminAuth.verifyPin(pin); if (!isValid) { console.log(t('adminCli.invalidPin')); await this.prompt(t('menu.pressEnterToContinue')); await this.showInteractiveMenu(); return; } console.log(t('adminCli.accessGranted')); } console.log(t('summary.status.generating')); try { // Use SummaryService instead of direct import this.summaryService.initialize(configManager); const report = await this.summaryService.run({ fromMenu: true }); console.log(report); console.log(t('summary.status.completed')); // Check if we're in interactive mode before prompting if (!this.isNonInteractiveMode()) { try { await this.prompt('\n' + t('debug.pressEnterToContinue')); await this.showInteractiveMenu(); } catch (error) { console.log(t('menu.returning')); process.exit(0); } } else { console.log(t('status.exitingCompleted')); process.exit(0); } } catch (error) { console.error(t('common.errorGeneratingStatusSummary', { error: error.message })); // Check if we're in interactive mode before prompting if (!this.isNonInteractiveMode()) { try { await this.prompt('\n' + t('debug.pressEnterToContinue')); await this.showInteractiveMenu(); } catch (error) { console.log(t('menu.returning')); process.exit(0); } } else { console.log(t('common.errorExiting')); process.exit(1); } } break; case '9': await this.deleteReports(); break; case '10': await this.showSettingsMenu(); break; case '11': this.showHelp(); await this.prompt(t('menu.returnToMainMenu')); await this.showInteractiveMenu(); break; case '12': await this.showLanguageMenu(); break; case '13': await this.executeCommand('scanner', {fromMenu: true}); await this.showInteractiveMenu(); return; case '14': await this.executeCommand('translate', {fromMenu: true}); await this.showInteractiveMenu(); return; case '0': console.log(t('menu.goodbye')); this.safeClose(); process.exit(0); default: console.log(t('menu.invalidChoice')); await this.showInteractiveMenu(); } } // ... existing code for showLanguageMenu, showDebugMenu, deleteReports, showSettingsMenu, etc. ... async showLanguageMenu() { const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); const currentLanguage = settings.uiLanguage || settings.language || this.config.uiLanguage || this.config.language || 'en'; const languages = configManager.getAvailableLanguages ? configManager.getAvailableLanguages() : [ { code: 'en', name: 'English' } ]; const currentLanguageName = languages.find(lang => lang.code === currentLanguage)?.name || currentLanguage; console.log(`\n${t('language.title')}`); console.log(t('language.separator')); console.log(t('language.current', { language: currentLanguageName })); console.log('\n' + t('language.available')); languages.forEach((lang, index) => { const current = lang.code === currentLanguage ? ' ✓' : ''; console.log(t('language.languageOption', { index: index + 1, displayName: lang.name, current })); }); console.log(`0. ${t('language.backToMainMenu')}`); const languagePrompt = formatLanguagePrompt(t('language.prompt'), languages.length); const choice = await this.prompt('\n' + languagePrompt); const choiceNum = parseInt(choice); if (choiceNum === 0) { await this.showInteractiveMenu(); return; } else if (choiceNum >= 1 && choiceNum <= languages.length) { const selectedLang = languages[choiceNum - 1]; settings.language = selectedLang.code; settings.uiLanguage = selectedLang.code; if (configManager.saveSettings) { await configManager.saveSettings(settings); } else if (configManager.saveConfig) { await configManager.saveConfig(settings); } console.log(t('language.changed', { language: selectedLang.name })); // Force reload translations for the entire system loadTranslations(selectedLang.code); // Return to main menu with new language await this.prompt('\n' + t('language.pressEnterToContinue')); await this.showInteractiveMenu(); } else { console.log(t('language.invalid')); await this.prompt('\n' + t('language.pressEnterToContinue')); await this.showLanguageMenu(); } } async showDebugMenu() { // Check for PIN protection const authRequired = await this.adminAuth.isAuthRequiredForScript('debugMenu'); if (authRequired) { console.log(`\n${t('adminPin.protectedAccess')}`); const pin = await cliHelper.promptPin(t('adminPin.enterPin') + ': '); const isValid = await this.adminAuth.verifyPin(pin); if (!isValid) { console.log(t('adminPin.invalidPin')); await this.prompt(t('menu.pressEnterToContinue')); await this.showInteractiveMenu(); return; } console.log(t('adminPin.accessGranted')); } console.log(`\n${t('debug.title')}`); console.log(t('debug.separator')); console.log(t('debug.mainDebuggerSystemDiagnostics')); console.log(t('debug.debugLogs')); console.log(t('debug.backToMainMenu')); const choice = await this.prompt('\n' + t('debug.selectOption')); switch (choice.trim()) { case '1': await this.runDebugTool('debugger.js', 'Main Debugger'); break; case '2': await this.viewDebugLogs(); break; case '0': await this.showInteractiveMenu(); return; default: console.log(t('debug.invalidChoiceSelectRange')); await this.showDebugMenu(); } } async runDebugTool(toolName, displayName) { console.log(t('debug.runningDebugTool', { displayName })); try { const toolPath = path.join(__dirname, '..', '..', 'scripts', 'debug', toolName); if (SecurityUtils.safeExistsSync(toolPath)) { console.log(`Debug tool available: ${toolName}`); console.log(`To run this tool manually: node "${toolPath}"`); console.log(`Working directory: ${path.join(__dirname, '..', '..')}`); } else { console.log(t('debug.debugToolNotFound', { toolName })); } } catch (error) { console.error(t('debug.errorRunningDebugTool', { displayName, error: error.message })); } await this.prompt('\n' + t('menu.pressEnterToContinue')); await this.showDebugMenu(); } async viewDebugLogs() { console.log(`\n${t('debug.recentDebugLogs')}`); console.log('============================================================'); try { const logsDir = path.join(__dirname, '..', '..', 'scripts', 'debug', 'logs'); const validatedLogsDir = SecurityUtils.validatePath(logsDir, process.cwd()); if (validatedLogsDir && SecurityUtils.safeExistsSync(validatedLogsDir, process.cwd())) { const files = fs.readdirSync(validatedLogsDir) .filter(file => file.endsWith('.log') || file.endsWith('.txt')) .sort((a, b) => { const statA = fs.statSync(path.join(validatedLogsDir, a)); const statB = fs.statSync(path.join(validatedLogsDir, b));