UNPKG

dbshift

Version:

A simple and powerful MySQL database migration tool inspired by Flyway

1,067 lines (961 loc) 53.3 kB
const React = require('react'); const { render, Box, useInput, useApp, Spacer, Static } = require('ink'); const { useStdout } = require('ink'); const { useState, useEffect, useCallback } = React; const package = require('../../package.json'); // Import dialog components const InitDialog = require('./components/dialogs/InitDialog'); const CreateDialog = require('./components/dialogs/CreateDialog'); const ConfigDialog = require('./components/dialogs/ConfigDialog'); const ConfigInitDialog = require('./components/dialogs/ConfigInitDialog'); const ConfigSetDialog = require('./components/dialogs/ConfigSetDialog'); const Config = require('../core/config'); // Import layout components const { DBShiftLogo, WelcomeTips, InputBox, UserInputBox, CommandOutputBox, CommandSuggestions } = require('./components/Layout'); function startInteractiveMode() { // 检查是否支持交互模式 if (!process.stdin.isTTY) { console.log('🚀 DBShift Interactive Mode'); console.log(''); console.log('❌ Interactive mode requires a terminal (TTY)'); console.log('💡 Use CLI mode instead: dbshift -p -- <command>'); console.log(''); console.log('Examples:'); console.log(' dbshift -p -- status'); console.log(' dbshift -p -- create "test migration"'); console.log(' dbshift -p -- migrate'); console.log(''); console.log('For help: dbshift --help'); process.exit(1); } // 捕获控制台输出的辅助函数 const captureConsoleOutput = (fn) => { const originalLog = console.log; const originalError = console.error; const outputs = []; console.log = (...args) => { outputs.push({ type: 'info', content: args.join(' ') }); originalLog(...args); }; console.error = (...args) => { outputs.push({ type: 'error', content: args.join(' ') }); originalError(...args); }; return new Promise((resolve) => { Promise.resolve(fn()).then(() => { console.log = originalLog; console.error = originalError; resolve(outputs); }).catch((error) => { console.log = originalLog; console.error = originalError; outputs.push({ type: 'error', content: error.message }); resolve(outputs); }); }); }; // 导入命令处理器 const initCommand = require('../commands/init'); const migrateCommand = require('../commands/migrate'); const statusCommand = require('../commands/status'); const createCommand = require('../commands/create'); const historyCommand = require('../commands/history'); const showConfigCommand = require('../commands/config/index'); const configInitCommand = require('../commands/config/init'); const configSetCommand = require('../commands/config/set'); const testConnectionCommand = require('../commands/test-connection'); // 设置交互模式环境变量 process.env.DBSHIFT_INTERACTIVE_MODE = 'true'; // 持久化历史记录管理 const HISTORY_FILE = '.dbshift_history'; const MAX_HISTORY_SIZE = 1000; // 增加历史记录容量到1000条 const loadHistory = () => { try { const fs = require('fs'); const path = require('path'); const historyPath = path.join(process.cwd(), HISTORY_FILE); // 🔄 每次启动时清空历史记录文件,开始全新的会话 if (fs.existsSync(historyPath)) { fs.writeFileSync(historyPath, '', 'utf8'); // 清空文件内容 } // 返回空的历史记录数组,开始新会话 return []; } catch (error) { // 忽略历史记录处理错误,继续正常运行 } return []; }; const saveHistory = (newHistory) => { try { const fs = require('fs'); const path = require('path'); const historyPath = path.join(process.cwd(), HISTORY_FILE); // 保持完整顺序,允许重复命令,只限制大小 const limitedHistory = newHistory.slice(-MAX_HISTORY_SIZE); // 保留最新的记录 fs.writeFileSync(historyPath, limitedHistory.join('\n') + '\n', 'utf8'); } catch (error) { // 忽略历史记录保存错误,继续正常运行 } }; // 主应用组件 - 加入 Gemini CLI 风格的布局控制 const DBShiftApp = () => { const [input, setInput] = useState(''); const [cursorPosition, setCursorPosition] = useState(0); // New state for cursor position const [conversations, setConversations] = useState([]); const [suggestions, setSuggestions] = useState([]); const [selectedSuggestion, setSelectedSuggestion] = useState(0); const [scrollOffset, setScrollOffset] = useState(0); // New state for scroll offset const VISIBLE_SUGGESTIONS_COUNT = 8; // New constant for visible suggestions const [history, setHistory] = useState(() => loadHistory()); // 启动时加载历史记录 const [historyIndex, setHistoryIndex] = useState(-1); const [isProcessing, setIsProcessing] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false); // 对话框状态管理 const [showInitDialog, setShowInitDialog] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false); // New state for CreateDialog const [showConfigDialog, setShowConfigDialog] = useState(false); // New state for ConfigDialog const [showConfigInitDialog, setShowConfigInitDialog] = useState(false); // New state for ConfigInitDialog const [showConfigSetDialog, setShowConfigSetDialog] = useState(false); // New state for ConfigSetDialog const { exit } = useApp(); const { stdout } = useStdout(); // 捕获控制台输出的辅助函数 - 移到前面避免引用错误 const captureConsoleOutput = useCallback((fn) => { const originalLog = console.log; const originalError = console.error; const outputs = []; console.log = (...args) => { outputs.push({ type: 'info', content: args.join(' ') }); // 不再调用 originalLog,避免重复显示 }; console.error = (...args) => { outputs.push({ type: 'error', content: args.join(' ') }); // 不再调用 originalError,避免重复显示 }; return new Promise((resolve) => { Promise.resolve(fn()).then(() => { console.log = originalLog; console.error = originalError; resolve(outputs); }).catch((error) => { console.log = originalLog; console.error = originalError; outputs.push({ type: 'error', content: error.message }); resolve(outputs); }); }); }, []); // Gemini CLI 风格:终端尺寸检测和布局控制 const terminalHeight = stdout.rows || 24; const terminalWidth = stdout.columns || 80; // Gemini CLI 风格:不使用定时器闪烁,依赖终端原生行为 // 光标始终可见,使用 chalk.inverse() 创建稳定的光标效果 const cursorVisible = true; // 可用命令 const commands = [ { command: '/init', description: 'Initialize migration environment' }, { command: '/migrate', description: 'Run pending migrations' }, { command: '/status', description: 'Show migration status' }, { command: '/create', description: 'Create a new migration file' }, { command: '/history', description: 'Show migration execution history' }, { command: '/config', description: 'Show current configuration' }, { command: '/config-init', description: 'Interactive configuration setup' }, { command: '/config-set', description: 'Set configuration values' }, { command: '/ping', description: 'Test database connection' }, { command: '/about', description: 'Show version information' }, { command: '/help', description: 'Show available commands' }, { command: '/exit', description: 'Exit interactive mode' } ]; // 添加对话 const addConversation = useCallback((userInput, outputs) => { setConversations(prev => [...prev, { userInput, outputs, timestamp: Date.now() }]); }, []); // 对话框处理函数 const handleInitDialogComplete = useCallback((result) => { setShowInitDialog(false); let finalOutputs = []; if (result.cancelled) { finalOutputs = [ { type: 'warning', content: '⚠️ Initialization cancelled' }, { type: 'info', content: 'Project initialization was cancelled by user.' } ]; } else if (result.useCliMode) { finalOutputs = [ { type: 'info', content: '🔄 Switching to CLI mode for advanced configuration...' }, { type: 'success', content: 'Run: dbshift -p -- init' }, { type: 'info', content: 'This will provide full interactive experience with all prompts.' } ]; } else { // 处理配置文件创建 try { const fs = require('fs'); const { dbConfig, configType } = result; if (!dbConfig) { finalOutputs = [{ type: 'error', content: '❌ No database configuration provided' }]; } else { if (configType === 'env') { const envContent = `MYSQL_HOST=${dbConfig.host} MYSQL_PORT=${dbConfig.port} MYSQL_USERNAME=${dbConfig.username} MYSQL_PASSWORD=${dbConfig.password} `; fs.writeFileSync('.env', envContent); finalOutputs = [ { type: 'success', content: '✅ Project initialized successfully!' }, { type: 'info', content: '📁 Created .env configuration file' }, { type: 'info', content: '📁 Created migrations/ directory' }, { type: 'info', content: 'Next steps: Create migrations with /create command' } ]; } else { const configContent = `module.exports = { development: { host: '${dbConfig.host}', port: ${dbConfig.port}, user: '${dbConfig.username}', password: '${dbConfig.password}', database: 'your_database_name' }, production: { host: process.env.MYSQL_HOST || '${dbConfig.host}', port: process.env.MYSQL_PORT || ${dbConfig.port}, user: process.env.MYSQL_USERNAME || '${dbConfig.username}', password: process.env.MYSQL_PASSWORD || '${dbConfig.password}', database: process.env.MYSQL_DATABASE || 'your_database_name' } }; `; fs.writeFileSync('schema.config.js', configContent); finalOutputs = [ { type: 'success', content: '✅ Project initialized successfully!' }, { type: 'info', content: '📁 Created schema.config.js configuration file' }, { type: 'info', content: '📁 Created migrations/ directory' }, { type: 'info', content: '💡 Edit schema.config.js to add your database name' }, { type: 'info', content: 'Next steps: Create migrations with /create command' } ]; } if (!fs.existsSync('migrations')) { fs.mkdirSync('migrations'); } } } catch (error) { finalOutputs = [{ type: 'error', content: `❌ Error creating configuration: ${error.message}` }]; } } // Update the last conversation instead of creating a new one setConversations(prev => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; const updatedLast = { ...last, outputs: finalOutputs }; return [...prev.slice(0, -1), updatedLast]; }); }, [setConversations]); const handleInitDialogCancel = useCallback(() => { setShowInitDialog(false); // Update the last conversation with the cancellation message setConversations(prev => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; const updatedLast = { ...last, outputs: [{ type: 'info', content: '❌ Initialization cancelled' }] }; return [...prev.slice(0, -1), updatedLast]; }); }, [setConversations]); // CreateDialog 处理函数 const handleCreateDialogComplete = useCallback(async (result) => { setShowCreateDialog(false); setIsProcessing(false); // 立即重置处理状态 let finalOutputs = []; if (result.cancelled) { finalOutputs = [{ type: 'warning', content: '⚠️ Migration creation cancelled' }]; } else { try { finalOutputs = await captureConsoleOutput(() => createCommand(result.name, { author: result.author, migrationType: result.type })); } catch (error) { finalOutputs = [{ type: 'error', content: `❌ Error creating migration: ${error.message}` }]; } } setConversations(prev => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; const updatedLast = { ...last, outputs: finalOutputs }; return [...prev.slice(0, -1), updatedLast]; }); }, [captureConsoleOutput, setConversations]); const handleCreateDialogCancel = useCallback(() => { setShowCreateDialog(false); setIsProcessing(false); // 立即重置处理状态 setConversations(prev => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; const updatedLast = { ...last, outputs: [{ type: 'info', content: '❌ Migration creation cancelled' }] }; return [...prev.slice(0, -1), updatedLast]; }); }, [setConversations]); // ConfigDialog 处理函数 const handleConfigDialogComplete = useCallback(async (result) => { setShowConfigDialog(false); setIsProcessing(false); let finalOutputs = []; if (result.cancelled) { finalOutputs = [{ type: 'warning', content: '⚠️ Configuration view cancelled' }]; } else { try { finalOutputs = await captureConsoleOutput(() => showConfigCommand({ env: result.environment })); } catch (error) { finalOutputs = [{ type: 'error', content: `❌ Error loading configuration: ${error.message}` }]; } } setConversations(prev => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; const updatedLast = { ...last, outputs: finalOutputs }; return [...prev.slice(0, -1), updatedLast]; }); }, [captureConsoleOutput, setConversations]); const handleConfigDialogCancel = useCallback(() => { setShowConfigDialog(false); setIsProcessing(false); setConversations(prev => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; const updatedLast = { ...last, outputs: [{ type: 'info', content: '❌ Configuration view cancelled' }] }; return [...prev.slice(0, -1), updatedLast]; }); }, [setConversations]); // ConfigInitDialog 处理函数 const handleConfigInitDialogComplete = useCallback(async (result) => { // 🔑 立即设置状态,确保对话框隐藏 setShowConfigInitDialog(false); setIsProcessing(false); let finalOutputs = []; if (result.cancelled) { finalOutputs = [{ type: 'warning', content: '⚠️ Configuration setup cancelled' }]; } else { // 🔑 修复:直接在这里处理配置创建,避免调用 inquirer try { const fs = require('fs'); const path = require('path'); const { dbConfig, configType } = result; finalOutputs.push({ type: 'info', content: `✓ Using ${configType === 'env' ? '.env' : 'schema.config.js'} format` }); finalOutputs.push({ type: 'info', content: ` Host: ${dbConfig.host}:${dbConfig.port}` }); finalOutputs.push({ type: 'info', content: ` User: ${dbConfig.username}` }); finalOutputs.push({ type: 'info', content: ` Password: ${dbConfig.password ? '***' : 'N/A'}` }); if (configType === 'env') { const envContent = `### MySQL Database Configuration MYSQL_HOST=${dbConfig.host} MYSQL_PORT=${dbConfig.port} MYSQL_USERNAME=${dbConfig.username} MYSQL_PASSWORD=${dbConfig.password} # For production deployment, override these with environment variables: # export MYSQL_HOST=your-prod-host # export MYSQL_USERNAME=your-prod-user # export MYSQL_PASSWORD=your-prod-password `; fs.writeFileSync('.env', envContent); finalOutputs.push({ type: 'success', content: '✓ Created .env configuration file' }); finalOutputs.push({ type: 'info', content: '💡 For production, use environment variables to override .env values' }); } else { const configContent = `module.exports = { development: { host: '${dbConfig.host}', port: ${dbConfig.port}, user: '${dbConfig.username}', password: '${dbConfig.password}' }, staging: { host: '${dbConfig.host}', port: ${dbConfig.port}, user: '${dbConfig.username}', password: '${dbConfig.password}' }, production: { host: process.env.MYSQL_HOST || '${dbConfig.host}', port: process.env.MYSQL_PORT || ${dbConfig.port}, user: process.env.MYSQL_USERNAME || '${dbConfig.username}', password: process.env.MYSQL_PASSWORD || '${dbConfig.password}' } }; `; fs.writeFileSync('schema.config.js', configContent); finalOutputs.push({ type: 'success', content: '✓ Created schema.config.js configuration file' }); finalOutputs.push({ type: 'info', content: '💡 Use "dbshift migrate -e production" to run with production config' }); finalOutputs.push({ type: 'info', content: '💡 Set environment variables for production: MYSQL_HOST, MYSQL_USERNAME, etc.' }); } finalOutputs.push({ type: 'success', content: '🎉 Database configuration initialized successfully!' }); } catch (error) { finalOutputs = [{ type: 'error', content: `❌ Error creating configuration: ${error.message}` }]; } } setConversations(prev => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; const updatedLast = { ...last, outputs: finalOutputs }; return [...prev.slice(0, -1), updatedLast]; }); }, [setConversations]); const handleConfigInitDialogCancel = useCallback(() => { setShowConfigInitDialog(false); setIsProcessing(false); setConversations(prev => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; const updatedLast = { ...last, outputs: [{ type: 'info', content: '❌ Configuration setup cancelled' }] }; return [...prev.slice(0, -1), updatedLast]; }); }, [setConversations]); // ConfigSetDialog 处理函数 const handleConfigSetDialogComplete = useCallback(async (result) => { setShowConfigSetDialog(false); setIsProcessing(false); let finalOutputs = []; if (result.cancelled) { finalOutputs = [{ type: 'warning', content: '⚠️ Configuration update cancelled' }]; } else { // 🔑 修复:直接在这里处理配置更新,避免调用外部命令 try { const fs = require('fs'); const path = require('path'); const targetEnv = result.environment || 'development'; finalOutputs.push({ type: 'info', content: `⚙️ Updating database configuration for [${targetEnv}] environment...` }); finalOutputs.push({ type: 'info', content: ` Environment: ${targetEnv}` }); finalOutputs.push({ type: 'info', content: ` Host: ${result.host}:${result.port}` }); finalOutputs.push({ type: 'info', content: ` User: ${result.username}` }); finalOutputs.push({ type: 'info', content: ` Password: ${result.password ? '***' : 'N/A'}` }); const envPath = path.join(process.cwd(), '.env'); const configPath = path.join(process.cwd(), 'schema.config.js'); if (fs.existsSync(configPath)) { // 更新 schema.config.js let configContent = {}; try { configContent = require(configPath); } catch (error) { configContent = {}; } if (!configContent[targetEnv]) { configContent[targetEnv] = {}; } configContent[targetEnv].host = result.host; configContent[targetEnv].port = parseInt(result.port); configContent[targetEnv].user = result.username; configContent[targetEnv].password = result.password; // 生成配置文件内容,保留现有环境配置 const generateConfigContent = (config) => { let content = 'module.exports = {\n'; // 确保基本环境存在 const environments = ['development', 'staging', 'production']; environments.forEach(env => { if (config[env] || env === targetEnv) { const envConfig = config[env] || config[targetEnv]; content += ` ${env}: {\n`; content += ` host: '${envConfig.host}',\n`; content += ` port: ${envConfig.port},\n`; content += ` user: '${envConfig.user}',\n`; if (env === 'production') { content += ` password: process.env.MYSQL_PASSWORD || '${envConfig.password}'\n`; } else { content += ` password: '${envConfig.password}'\n`; } content += ` }${env === 'production' ? '' : ','}\n`; } }); content += '};\n'; return content; }; const configFileContent = generateConfigContent(configContent); fs.writeFileSync(configPath, configFileContent); finalOutputs.push({ type: 'success', content: `✓ Updated schema.config.js for [${targetEnv}] environment` }); } else if (fs.existsSync(envPath)) { // 更新 .env 文件 const newEnvContent = `### MySQL Database Configuration MYSQL_HOST=${result.host} MYSQL_PORT=${result.port} MYSQL_USERNAME=${result.username} MYSQL_PASSWORD=${result.password} # For production deployment, override these with environment variables: # export MYSQL_HOST=your-prod-host # export MYSQL_USERNAME=your-prod-user # export MYSQL_PASSWORD=your-prod-password `; fs.writeFileSync(envPath, newEnvContent); finalOutputs.push({ type: 'success', content: '✓ Updated .env configuration file' }); } else { // 创建新的 .env 文件 const newEnvContent = `### MySQL Database Configuration MYSQL_HOST=${result.host} MYSQL_PORT=${result.port} MYSQL_USERNAME=${result.username} MYSQL_PASSWORD=${result.password} # For production deployment, override these with environment variables: # export MYSQL_HOST=your-prod-host # export MYSQL_USERNAME=your-prod-user # export MYSQL_PASSWORD=your-prod-password `; fs.writeFileSync(envPath, newEnvContent); finalOutputs.push({ type: 'success', content: '✓ Created .env configuration file' }); } finalOutputs.push({ type: 'success', content: '🎉 Database configuration updated successfully!' }); } catch (error) { finalOutputs = [{ type: 'error', content: `❌ Error updating configuration: ${error.message}` }]; } } setConversations(prev => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; const updatedLast = { ...last, outputs: finalOutputs }; return [...prev.slice(0, -1), updatedLast]; }); }, [captureConsoleOutput, setConversations]); const handleConfigSetDialogCancel = useCallback(() => { setShowConfigSetDialog(false); setIsProcessing(false); setConversations(prev => { if (prev.length === 0) return prev; const last = prev[prev.length - 1]; const updatedLast = { ...last, outputs: [{ type: 'info', content: '❌ Configuration update cancelled' }] }; return [...prev.slice(0, -1), updatedLast]; }); }, [setConversations]); const updateSuggestions = useCallback((inputValue) => { if (inputValue.startsWith('/')) { // 斜杠命令模式 - 显示以输入开头的命令 const filtered = commands.filter(cmd => cmd.command.toLowerCase().startsWith(inputValue.toLowerCase()) ); // 只有在真正变化时才更新状态 setSuggestions(prev => { if (prev.length !== filtered.length || prev.some((cmd, index) => cmd.command !== filtered[index]?.command)) { return filtered; } return prev; }); setSelectedSuggestion(0); setShowSuggestions(filtered.length > 0); } else if (inputValue.trim()) { // 普通模式,只显示完全匹配的命令 const filtered = commands.filter(cmd => cmd.command.toLowerCase().startsWith(inputValue.toLowerCase()) ); setSuggestions(prev => { if (prev.length !== filtered.length || prev.some((cmd, index) => cmd.command !== filtered[index]?.command)) { return filtered; } return prev; }); setSelectedSuggestion(0); setShowSuggestions(filtered.length > 0); } else { setSuggestions([]); setShowSuggestions(false); } }, []); // 执行命令 const executeCommand = useCallback(async (command) => { const parts = command.trim().split(' '); const cmdWithSlash = parts[0].toLowerCase(); const cmd = cmdWithSlash.startsWith('/') ? cmdWithSlash.slice(1) : cmdWithSlash; const args = parts.slice(1); setIsProcessing(true); // 格式化用户输入显示(保留斜杠) const displayCommand = command.startsWith('/') ? command : `/${command}`; let outputs = []; try { switch (cmd) { case 'init': // 显示 Init 对话框 - 按照 Gemini CLI 方式,命令保留在历史中 setShowInitDialog(true); setIsProcessing(false); // 重置处理状态,让对话框处理输入 outputs = [{ type: 'info', content: '🚀 Opening initialization dialog...' }]; // 对于对话框命令,先添加对话记录再返回,避免重复添加 addConversation(displayCommand, outputs); return; case 'migrate': const env = args.find(arg => arg.startsWith('--env='))?.split('=')[1] || 'development'; outputs = await captureConsoleOutput(() => migrateCommand({ env })); break; case 'status': const statusEnv = args.find(arg => arg.startsWith('--env='))?.split('=')[1] || 'development'; outputs = await captureConsoleOutput(() => statusCommand({ env: statusEnv })); break; case 'create': // 显示 Create 对话框 setShowCreateDialog(true); setIsProcessing(false); // 重置处理状态,让对话框处理输入 outputs = [{ type: 'info', content: '📝 Opening create migration dialog...' }]; // 对于对话框命令,先添加对话记录再返回,避免重复添加 addConversation(displayCommand, outputs); return; case 'history': const historyEnv = args.find(arg => arg.startsWith('--env='))?.split('=')[1] || 'development'; const authorFilter = args.find(arg => arg.startsWith('--author='))?.split('=')[1]; outputs = await captureConsoleOutput(() => historyCommand({ env: historyEnv, author: authorFilter })); break; case 'config': // 检查是否指定了环境参数 const configEnv = args.find(arg => arg.startsWith('--env='))?.split('=')[1]; if (configEnv) { // 如果指定了环境,直接显示 outputs = await captureConsoleOutput(() => showConfigCommand({ env: configEnv })); } else { // 如果没有指定环境,显示选择对话框 setShowConfigDialog(true); setIsProcessing(false); outputs = [{ type: 'info', content: '⚙️ Opening configuration view dialog...' }]; addConversation(displayCommand, outputs); return; } break; case 'config-init': // 显示 ConfigInit 对话框 setShowConfigInitDialog(true); setIsProcessing(false); // 重置处理状态,让对话框处理输入 outputs = [{ type: 'info', content: '⚙️ Opening configuration setup dialog...' }]; // 对于对话框命令,先添加对话记录再返回,避免重复添加 addConversation(displayCommand, outputs); return; case 'config-set': // 检查是否有命令行参数 const hasCliArgs = args.some(arg => arg.startsWith('--host=') || arg.startsWith('--port=') || arg.startsWith('--user=') || arg.startsWith('--password=') || arg.startsWith('--database=')); if (hasCliArgs) { // 如果有命令行参数,直接执行 configSetCommand const configOptions = { host: args.find(arg => arg.startsWith('--host='))?.split('=')[1], port: args.find(arg => arg.startsWith('--port='))?.split('=')[1], user: args.find(arg => arg.startsWith('--user='))?.split('=')[1], password: args.find(arg => arg.startsWith('--password='))?.split('=')[1], database: args.find(arg => arg.startsWith('--database='))?.split('=')[1], env: args.find(arg => arg.startsWith('--env='))?.split('=')[1] || 'development' }; outputs = await captureConsoleOutput(() => configSetCommand(configOptions)); } else { // 如果没有命令行参数,显示 ConfigSet 对话框 setShowConfigSetDialog(true); setIsProcessing(false); // 重置处理状态,让对话框处理输入 outputs = [{ type: 'info', content: '⚙️ Opening configuration editor dialog...' }]; // 对于对话框命令,先添加对话记录再返回,避免重复添加 addConversation(displayCommand, outputs); return; } break; case 'ping': const pingEnv = args.find(arg => arg.startsWith('--env='))?.split('=')[1] || 'development'; const pingOptions = { env: pingEnv, host: args.find(arg => arg.startsWith('--host='))?.split('=')[1], port: args.find(arg => arg.startsWith('--port='))?.split('=')[1], user: args.find(arg => arg.startsWith('--user='))?.split('=')[1], password: args.find(arg => arg.startsWith('--password='))?.split('=')[1], database: args.find(arg => arg.startsWith('--database='))?.split('=')[1] }; outputs = await captureConsoleOutput(() => testConnectionCommand(pingOptions)); break; case 'about': outputs = [ { type: 'info', content: 'About DBShift CLI' }, { type: 'info', content: '' }, { type: 'info', content: 'CLI Version ' + package.version }, { type: 'info', content: 'Database MySQL' }, { type: 'info', content: 'Framework Node.js' }, { type: 'info', content: 'Migration Tool Flyway-inspired' }, { type: 'info', content: 'License MIT' }, { type: 'info', content: 'Author greddy7574' } ]; break; case 'help': outputs = [ { type: 'info', content: '📋 Available Commands:' }, ...commands.map(cmd => ({ type: 'info', content: ` ${cmd.command.padEnd(15)} ${cmd.description}` })) ]; break; case 'exit': outputs = [{ type: 'info', content: '👋 Goodbye!' }]; addConversation(displayCommand, outputs); exit(); return; default: outputs = [{ type: 'error', content: `❌ Unknown command: ${cmd}. Type 'help' for available commands.` }]; break; } } catch (error) { outputs = [{ type: 'error', content: `❌ Error executing command: ${error.message}` }]; } finally { setIsProcessing(false); } // 添加对话记录 addConversation(displayCommand, outputs); }, [addConversation, exit, captureConsoleOutput]); // 处理命令提交 const handleSubmit = useCallback(async (command) => { if (!command.trim() || isProcessing) return; // 添加到历史记录(会话内累积,允许重复) setHistory(prev => { const updatedHistory = [...prev, command.trim()]; // 直接添加到末尾,保持完整顺序 // 异步保存历史记录 setTimeout(() => saveHistory(updatedHistory), 0); return updatedHistory.slice(-MAX_HISTORY_SIZE); // 限制历史记录大小 }); setHistoryIndex(-1); // 清空输入和建议 setInput(''); setSuggestions([]); setShowSuggestions(false); // 执行命令 await executeCommand(command); }, [executeCommand, isProcessing, addConversation]); // 删除字符的稳定回调 - 分离状态更新,确保光标位置正确 const handleDelete = useCallback(() => { setInput(prevInput => { if (prevInput.length === 0) return prevInput; // 重置其他状态 setHistoryIndex(-1); setShowSuggestions(false); setSuggestions([]); // 删除最后一个字符(退格键行为) return prevInput.slice(0, -1); }); // 分离光标位置更新 setCursorPosition(prevCursorPos => { if (input.length === 0) return 0; const validCursorPos = Math.max(0, Math.min(prevCursorPos, input.length)); return Math.max(0, validCursorPos - 1); }); }, [input.length]); // 键盘事件处理 - 只在没有对话框时监听 useInput(useCallback((inputChar, key) => { // 如果对话框显示,完全不处理输入,让对话框处理 if (showInitDialog || showCreateDialog || showConfigDialog || showConfigInitDialog || showConfigSetDialog) { return; } if (isProcessing) { return; } if (key.upArrow) { // 🔑 修复:优先使用历史记录导航,除非明确在建议模式下 if (history.length > 0 && (!showSuggestions || suggestions.length === 0)) { // 历史导航向上 - 改进的历史记录导航 let newIndex; if (historyIndex === -1) { // 从最新的历史记录开始 newIndex = history.length - 1; } else { // 向前查看更早的历史记录 newIndex = Math.max(0, historyIndex - 1); } setHistoryIndex(newIndex); const selectedCommand = history[newIndex]; setInput(selectedCommand); setCursorPosition(selectedCommand.length); // 移动光标到末尾 // 清除建议以避免干扰 setSuggestions([]); setShowSuggestions(false); } else if (showSuggestions && suggestions.length > 0) { // 在建议列表中向上导航 setSelectedSuggestion(prev => { const newIndex = prev > 0 ? prev - 1 : suggestions.length - 1; // 调整滚动偏移量 if (newIndex < scrollOffset) { setScrollOffset(newIndex); } else if (newIndex >= scrollOffset + VISIBLE_SUGGESTIONS_COUNT) { setScrollOffset(newIndex - VISIBLE_SUGGESTIONS_COUNT + 1); } return newIndex; }); } } else if (key.downArrow) { // 🔑 修复:优先使用历史记录导航,除非明确在建议模式下 if (historyIndex !== -1 && history.length > 0 && (!showSuggestions || suggestions.length === 0)) { // 历史导航向下 - 改进的历史记录导航 const newIndex = historyIndex + 1; if (newIndex < history.length) { // 还有更新的历史记录 setHistoryIndex(newIndex); const selectedCommand = history[newIndex]; setInput(selectedCommand); setCursorPosition(selectedCommand.length); // 移动光标到末尾 // 清除建议以避免干扰 setSuggestions([]); setShowSuggestions(false); } else { // 到达最新,回到空输入 setHistoryIndex(-1); setInput(''); setCursorPosition(0); // 移动光标到开头 setSuggestions([]); setShowSuggestions(false); } } else if (showSuggestions && suggestions.length > 0) { // 在建议列表中向下导航 setSelectedSuggestion(prev => { const newIndex = prev < suggestions.length - 1 ? prev + 1 : 0; // 调整滚动偏移量 if (newIndex >= scrollOffset + VISIBLE_SUGGESTIONS_COUNT) { setScrollOffset(newIndex - VISIBLE_SUGGESTIONS_COUNT + 1); } else if (newIndex < scrollOffset) { setScrollOffset(newIndex); } return newIndex; }); } } else if (key.leftArrow) { // 左方向键 setCursorPosition(prev => Math.max(0, prev - 1)); } else if (key.rightArrow) { // 右方向键 setCursorPosition(prev => Math.min(input.length, prev + 1)); } else if (key.tab) { // Tab 自动补全 if (suggestions.length > 0) { const selectedCommand = suggestions[selectedSuggestion].command; setInput(selectedCommand); setCursorPosition(selectedCommand.length); // 移动光标到末尾 updateSuggestions(selectedCommand); // 重置历史导航 setHistoryIndex(-1); } } else if (key.return) { // 提交命令 let commandToExecute = input; if (showSuggestions && suggestions.length > 0) { // 如果有建议,选择当前建议并执行 commandToExecute = suggestions[selectedSuggestion].command; setInput(commandToExecute); setCursorPosition(commandToExecute.length); // 移动光标到末尾 setShowSuggestions(false); setSuggestions([]); } // 重置历史导航 setHistoryIndex(-1); // 提交命令 handleSubmit(commandToExecute); } else if (key.escape) { // 隐藏建议,允许历史导航 setShowSuggestions(false); setSuggestions([]); // 🔑 修复:按Escape键时不重置历史导航状态 // setHistoryIndex(-1); // 移除这行,让用户可以在关闭建议后继续历史导航 } else if (key.ctrl && inputChar === 'c') { // Ctrl+C 退出 addConversation('/exit', [{ type: 'info', content: '👋 Goodbye!' }]); exit(); } else if (key.backspace || key.delete || inputChar === '\u0008' || inputChar === '\u007f') { // 删除字符 - 支持多种删除键变体,使用稳定的回调 handleDelete(); } else if (inputChar && inputChar.length === 1 && !key.ctrl && !key.meta && !key.tab && !key.return && !key.backspace && !key.delete && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow && !key.escape && inputChar !== '\u0008' && inputChar !== '\u007f') { // 普通字符输入 - 插入到光标位置 const charCode = inputChar.charCodeAt(0); if (charCode >= 32 && charCode <= 126) { // 🔑 修复光标位置:简化逻辑,分离状态更新 setInput(prevInput => { // 计算有效的插入位置(通常在末尾) const insertPos = Math.max(0, Math.min(cursorPosition, prevInput.length)); // 重置其他状态 setHistoryIndex(-1); setShowSuggestions(false); setSuggestions([]); // 插入字符到当前光标位置 return prevInput.slice(0, insertPos) + inputChar + prevInput.slice(insertPos); }); // 分离光标位置更新,确保与输入同步 setCursorPosition(prevCursorPos => { const validPos = Math.max(0, Math.min(prevCursorPos, input.length)); return validPos + 1; }); } } }, [input, history, historyIndex, suggestions, selectedSuggestion, showSuggestions, handleSubmit, isProcessing, exit, addConversation, updateSuggestions, showInitDialog, showCreateDialog, showConfigDialog, showConfigInitDialog, showConfigSetDialog, cursorPosition, handleDelete])); // 监听输入变化 - 优化防抖策略,提升快速输入时的响应性 useEffect(() => { const timeoutId = setTimeout(() => { updateSuggestions(input); }, input.length === 0 ? 30 : 80); // 进一步减少延迟,提升响应性 return () => clearTimeout(timeoutId); }, [input, updateSuggestions]); // 渲染界面 - 完全按照 Gemini CLI 的真实结构复制 return React.createElement(Box, { flexDirection: 'column', marginBottom: 1, width: '90%' }, // Static 区域 - 对应 Gemini CLI 的固定内容 React.createElement(Static, { key: 'staticContent', items: [ React.createElement(Box, { key: 'header', flexDirection: 'column' }, React.createElement(DBShiftLogo), React.createElement(WelcomeTips) ) ] }, (item) => item), // OverflowProvider 区域 - 对应 Gemini CLI 的滚动内容 React.createElement(Box, { flexDirection: 'column' }, conversations.map((conversation, index) => React.createElement(Box, { key: index, flexDirection: 'column', marginBottom: 1 }, React.createElement(UserInputBox, { command: conversation.userInput }), React.createElement(CommandOutputBox, { outputs: conversation.outputs }) ) ) ), // 主控制区域 - 完全对应 Gemini CLI 的 mainControlsRef React.createElement(Box, { flexDirection: 'column' }, // 条件渲染 - 完全按照 Gemini CLI 的三元运算符结构 showInitDialog ? ( React.createElement(Box, { flexDirection: 'column' }, React.createElement(InitDialog, { isVisible: showInitDialog, onComplete: handleInitDialogComplete, onCancel: handleInitDialogCancel }) ) ) : showCreateDialog ? ( React.createElement(Box, { flexDirection: 'column' }, React.createElement(CreateDialog, { isVisible: showCreateDialog, onComplete: handleCreateDialogComplete, onCancel: handleCreateDialogCancel }) ) ) : showConfigDialog ? ( React.createElement(Box, { flexDirection: 'column' }, React.createElement(ConfigDialog, { isVisible: showConfigDialog, onComplete: handleConfigDialogComplete, onCancel: handleConfigDialogCancel }) ) ) : showConfigInitDialog ? ( React.createElement(Box, { flexDirection: 'column' }, React.createElement(ConfigInitDialog, { isVisible: showConfigInitDialog, onComplete: handleConfigInitDialogComplete, onCancel: handleConfigInitDialogCancel }) ) ) : showConfigSetDialog ? ( React.createElement(Box, { flexDirection: 'column' }, React.createElement(ConfigSetDialog, { isVisible: showConfigSetDialog, onComplete: handleConfigSetDialogComplete, onCancel: handleConfigSetDialogCancel, initialConfig: {} }) ) ) : ( React.createElement(React.Fragment, {}, React.createElement(InputBox, { value: input, showCursor: cursorVisible, cursorPosition: cursorPosition }), showSuggestions && React.createElement(CommandSuggestions, { commands: suggestions, selectedIndex: selectedSuggestion, showCount: true,