UNPKG

i18ntk

Version:

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

348 lines (303 loc) 15.4 kB
#!/usr/bin/env node /** * setup-enforcer.js - Setup Completion Enforcement * * Ensures that setup is complete before allowing any script to run. * Provides interactive setup prompting for a better user experience. */ const { getIcon } = require('./terminal-icons'); const fs = require('fs'); const path = require('path'); const { blue, yellow, gray, cyan, green, red } = require('./colors-new'); const SecurityUtils = require('./security'); const I18nSetupModule = require('../main/i18ntk-setup'); const { envManager } = require('./env-manager'); const INTERACTIVE_PROMPT_TIMEOUT_MS = 5 * 60 * 1000; async function runSetupModule() { if (I18nSetupModule && typeof I18nSetupModule.run === 'function') { return await I18nSetupModule.run(); } if (typeof I18nSetupModule === 'function') { const setupManager = new I18nSetupModule(); if (typeof setupManager.setup === 'function') { return await setupManager.setup(); } } throw new Error('Setup module does not expose a runnable entrypoint'); } class SetupEnforcer { static _setupCheckInProgress = false; static _setupCheckPromise = null; /** * Detect if running in non-interactive environment * @returns {boolean} - True if non-interactive */ static isNonInteractive() { return !process.stdout.isTTY || !process.stdin.isTTY || envManager.getBoolean('CI') || envManager.getBoolean('CONTINUOUS_INTEGRATION') || envManager.get('NODE_ENV') === 'test' || envManager.get('npm_lifecycle_event') === 'test' || envManager.getBoolean('NO_INTERACTIVE'); } static checkSetupComplete() { // Avoid circular dependency - use direct path resolution const path = require('path'); const configPath = path.join(process.cwd(), '.i18ntk-config'); const exists = SecurityUtils.safeExistsSync(configPath); if (!exists) { this.handleMissingSetup(); return; } try { const configRaw = SecurityUtils.safeReadFileSync(configPath, path.dirname(configPath), 'utf8'); const config = SecurityUtils.safeParseJSON(configRaw); if (!config || typeof config !== 'object') { this.handleInvalidConfig(); return; } // Check if setup has been explicitly marked as completed if (config.setup && config.setup.completed === true) { return true; } // Fallback: check if config has required fields (for backward compatibility) if (!config.version || !config.sourceDir || (!config.detectedFramework && !(config.framework && config.framework.detected !== false))) { this.handleIncompleteSetup(); return; } return true; } catch (error) { this.handleInvalidConfig(); return; } } static async handleMissingSetup() { console.log(blue(`${getIcon('wrench')} Setup Required`)); console.log(yellow('Welcome to i18n Toolkit! This appears to be your first time running the toolkit.')); console.log(gray('Setup is required to configure your project for internationalization management.')); console.log(''); // Check if running in non-interactive environment if (SetupEnforcer.isNonInteractive()) { console.log(yellow('⚠️ Non-interactive environment detected.')); console.log(gray('Please run setup manually:')); console.log(cyan(' npm run i18ntk-setup')); console.log(gray('Or set NO_INTERACTIVE=false to force interactive mode.')); process.exit(1); } // Use readline for interactive prompt with timeout const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve, reject) => { // Give a human enough time to inspect setup output and answer. const timeout = setTimeout(() => { console.log(yellow('\n⏰ Timeout reached - no response received.')); console.log(gray('Setup cancelled. Run "npm run i18ntk-setup" when you\'re ready.')); rl.close(); process.exit(1); }, INTERACTIVE_PROMPT_TIMEOUT_MS); rl.question(cyan('Would you like to run setup now? (Y/n): '), async (answer) => { clearTimeout(timeout); rl.close(); if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') { console.log(gray('Setup cancelled. Run "npm run i18ntk-setup" when you\'re ready.')); process.exit(0); } console.log(green(`${getIcon('rocket')} Running setup...`)); try { // Import once at module scope and execute the stable entrypoint. const setupPath = path.join(__dirname, '..', 'main', 'i18ntk-setup.js'); if (!SecurityUtils.safeExistsSync(setupPath)) { console.error(red(`${getIcon('cross')} Setup script not found. Please run:`)); console.error(cyan(' npm run i18ntk-setup')); process.exit(1); } await runSetupModule(); console.log(gray('You can now run your original command.')); resolve(true); } catch (error) { console.error(red(`${getIcon('cross')} Error running setup:`), error.message); console.error(cyan(' npm run i18ntk-setup')); process.exit(1); } }); }); } static async handleIncompleteSetup() { console.log(blue(`${getIcon('wrench')} Incomplete Setup`)); console.log(yellow('Your setup appears to be incomplete or outdated.')); console.log(gray('This might happen after updating to a new version.')); console.log(''); // Check if running in non-interactive environment if (SetupEnforcer.isNonInteractive()) { console.log(yellow('⚠️ Non-interactive environment detected.')); console.log(gray('Please run setup manually:')); console.log(cyan(' npm run i18ntk-setup')); console.log(gray('Or set NO_INTERACTIVE=false to force interactive mode.')); process.exit(1); } const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve, reject) => { // Give a human enough time to inspect setup output and answer. const timeout = setTimeout(() => { console.log(yellow('\n⏰ Timeout reached - no response received.')); console.log(gray('Setup cancelled. Run "npm run i18ntk-setup" when you\'re ready.')); rl.close(); process.exit(1); }, INTERACTIVE_PROMPT_TIMEOUT_MS); rl.question(cyan('Would you like to re-run setup? (Y/n): '), async (answer) => { clearTimeout(timeout); rl.close(); if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') { console.log(gray('Operation cancelled.')); process.exit(0); } console.log(green(`${getIcon('rocket')} Running setup...`)); try { const setupPath = path.join(__dirname, '..', 'main', 'i18ntk-setup.js'); if (!SecurityUtils.safeExistsSync(setupPath)) { console.error(red(`${getIcon('cross')} Setup script not found. Please run:`)); console.error(cyan(' npm run i18ntk-setup')); process.exit(1); } await runSetupModule(); resolve(true); } catch (error) { console.error(red(`${getIcon('cross')} Error running setup:`), error.message); process.exit(1); } }); }); } static async handleInvalidConfig() { console.log(blue(`${getIcon('wrench')} Invalid Configuration`)); console.log(yellow('Your configuration file appears to be corrupted or invalid.')); console.log(gray('This might happen due to file corruption or manual editing.')); console.log(''); // Check if running in non-interactive environment if (SetupEnforcer.isNonInteractive()) { console.log(yellow('⚠️ Non-interactive environment detected.')); console.log(gray('Please run setup manually:')); console.log(cyan(' npm run i18ntk-setup')); console.log(gray('Or set NO_INTERACTIVE=false to force interactive mode.')); process.exit(1); } const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve, reject) => { // Give a human enough time to inspect setup output and answer. const timeout = setTimeout(() => { console.log(yellow('\n⏰ Timeout reached - no response received.')); console.log(gray('Setup cancelled. Run "npm run i18ntk-setup" when you\'re ready.')); rl.close(); process.exit(1); }, INTERACTIVE_PROMPT_TIMEOUT_MS); rl.question(cyan('Would you like to re-run setup to fix this? (Y/n): '), async (answer) => { clearTimeout(timeout); rl.close(); if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') { console.log(gray('Operation cancelled.')); process.exit(0); } console.log(green(`${getIcon('rocket')} Running setup...`)); try { const setupPath = path.join(__dirname, '..', 'main', 'i18ntk-setup.js'); if (!SecurityUtils.safeExistsSync(setupPath)) { console.error(red(`${getIcon('cross')} Setup script not found. Please run:`)); console.error(cyan(' npm run i18ntk-setup')); process.exit(1); } await runSetupModule(); resolve(true); } catch (error) { console.error(red(`${getIcon('cross')} Error running setup:`), error.message); process.exit(1); } }); }); } static checkSetupCompleteAsync() { // Return existing promise if already in progress // Add debugging for setup check if (SetupEnforcer._setupCheckInProgress && SetupEnforcer._setupCheckPromise) { return SetupEnforcer._setupCheckPromise; } // Create new promise and store it SetupEnforcer._setupCheckInProgress = true; SetupEnforcer._setupCheckPromise = (async () => { try { // Avoid circular dependency - use direct path resolution const pathModule = require('path'); const configPath = pathModule.join(process.cwd(), '.i18ntk-config'); const exists = SecurityUtils.safeExistsSync(configPath); if (!exists) { await SetupEnforcer.handleMissingSetup(); const existsAfter = SecurityUtils.safeExistsSync(configPath); if (existsAfter) { return true; } process.exit(0); } try { const configRaw = SecurityUtils.safeReadFileSync(configPath, pathModule.dirname(configPath), 'utf8'); const config = SecurityUtils.safeParseJSON(configRaw); if (!config || typeof config !== 'object') { await SetupEnforcer.handleInvalidConfig(); process.exit(0); } if (config.setup && config.setup.completed === true) { return true; } if (!config.version || !config.sourceDir || (!config.detectedFramework && !(config.framework && config.framework.detected !== false))) { await SetupEnforcer.handleIncompleteSetup(); const newConfigRaw = SecurityUtils.safeReadFileSync(configPath, pathModule.dirname(configPath), 'utf8'); const newConfig = SecurityUtils.safeParseJSON(newConfigRaw); if (!newConfig || typeof newConfig !== 'object') { process.exit(0); } if (newConfig.setup && newConfig.setup.completed === true) { return true; } else if (newConfig.version && newConfig.sourceDir && (newConfig.detectedFramework || (newConfig.framework && newConfig.framework.detected !== false))) { return true; } process.exit(0); } return true; } catch (error) { await SetupEnforcer.handleInvalidConfig(); try { const newConfigRaw = SecurityUtils.safeReadFileSync(configPath, pathModule.dirname(configPath), 'utf8'); const newConfig = SecurityUtils.safeParseJSON(newConfigRaw); if (!newConfig || typeof newConfig !== 'object') { process.exit(0); } if (newConfig.setup && newConfig.setup.completed === true) { return true; } else if (newConfig.version && newConfig.sourceDir && (newConfig.detectedFramework || (newConfig.framework && newConfig.framework.detected !== false))) { return true; } process.exit(0); } catch (e) { process.exit(0); } } } catch (error) { throw error; } finally { SetupEnforcer._setupCheckInProgress = false; } })(); return SetupEnforcer._setupCheckPromise; } } module.exports = SetupEnforcer;