import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import { execSync } from 'child_process' import readline from 'readline' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const projectRoot = path.dirname(__dirname) process.chdir(projectRoot) // 读取当前版本 function getCurrentVersion() { const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8')) return pkg.version } let currentVersion = getCurrentVersion() // 清屏并显示标题 function showHeader() { console.log('') console.log('+--------------------------------------------+') console.log('| 版本号设置工具 |') console.log('| Version Bump Tool |') console.log('+--------------------------------------------+') console.log('') console.log(`当前版本: ${currentVersion}`) console.log('') console.log('支持的输入格式:') console.log(' - 具体版本号: 1.0.0, 2.1.3') console.log(' - patch: 补丁版本 +1 (如 0.0.6 -> 0.0.7)') console.log(' - minor: 次版本 +1 (如 0.0.6 -> 0.1.0)') console.log(' - major: 主版本 +1 (如 0.0.6 -> 1.0.0)') console.log(' - 按 q 键退出') console.log('') } // 单键读取 function readKey() { return new Promise((resolve) => { const wasRaw = process.stdin.isRaw readline.emitKeypressEvents(process.stdin) if (process.stdin.isTTY) { process.stdin.setRawMode(true) } process.stdin.once('keypress', (ch, key) => { if (process.stdin.isTTY) { process.stdin.setRawMode(wasRaw) } resolve({ ch, key }) }) }) } // 读取输入,支持单键 q 退出 async function readInput(prompt) { process.stdout.write(prompt) let input = '' readline.emitKeypressEvents(process.stdin) if (process.stdin.isTTY) { process.stdin.setRawMode(true) } return new Promise((resolve) => { const onKeypress = (ch, key) => { if (key && key.name === 'return') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log('') resolve(input) } else if (key && key.name === 'escape') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log('') resolve('__EXIT__') } else if (key && key.name === 'backspace') { if (input.length > 0) { input = input.slice(0, -1) process.stdout.write('\b \b') } } else if (key && key.ctrl && key.name === 'c') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log('') process.exit(0) } else if (ch && /[a-zA-Z0-9.\-]/.test(ch)) { input += ch process.stdout.write(ch) // 单独输入 q 立即退出 if (input === 'q' || input === 'Q') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log('') resolve('__EXIT__') } } } process.stdin.on('keypress', onKeypress) }) } // 读取 Y/N async function readYesNo(prompt, defaultYes = true) { const hint = defaultYes ? 'Y/n' : 'y/N' process.stdout.write(`${prompt} (${hint}): `) readline.emitKeypressEvents(process.stdin) if (process.stdin.isTTY) { process.stdin.setRawMode(true) } return new Promise((resolve) => { const onKeypress = (ch, key) => { if (key && key.name === 'return') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log(defaultYes ? 'y' : 'n') resolve(defaultYes) } else if (ch === 'y' || ch === 'Y') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log('y') resolve(true) } else if (ch === 'n' || ch === 'N') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log('n') resolve(false) } else if (key && key.ctrl && key.name === 'c') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log('') process.exit(0) } } process.stdin.on('keypress', onKeypress) }) } // 等待按键继续或退出 async function waitForContinueOrExit() { process.stdout.write('按 Enter 继续修改,按 q 退出...') readline.emitKeypressEvents(process.stdin) if (process.stdin.isTTY) { process.stdin.setRawMode(true) } return new Promise((resolve) => { const onKeypress = (ch, key) => { if (key && key.name === 'return') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log('\n') resolve('continue') } else if (ch === 'q' || ch === 'Q') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log('\n') resolve('exit') } else if (key && key.ctrl && key.name === 'c') { process.stdin.removeListener('keypress', onKeypress) if (process.stdin.isTTY) process.stdin.setRawMode(false) console.log('') process.exit(0) } } process.stdin.on('keypress', onKeypress) }) } // 验证版本号格式 function validateVersion(input) { const keywords = ['patch', 'minor', 'major'] if (keywords.includes(input.toLowerCase())) return true return /^\d+\.\d+\.\d+$/.test(input) } // 获取新版本号(dry-run) function getNewVersion(input) { try { const result = execSync(`node scripts/bump-version.mjs ${input} --dry-run`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }) return result.trim() } catch { return null } } // 执行版本更新 function updateVersion(input) { try { execSync(`node scripts/bump-version.mjs ${input}`, { encoding: 'utf-8', stdio: 'inherit' }) return true } catch { return false } } // Git 提交 function gitCommit(version) { try { execSync('git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml src-tauri/Cargo.lock', { encoding: 'utf-8', stdio: 'inherit' }) execSync(`git commit -m "v${version}"`, { encoding: 'utf-8', stdio: 'inherit' }) return true } catch { return false } } // Git 添加 tag(如果已存在则先删除) function gitTag(version) { try { // 先尝试删除已存在的 tag try { execSync(`git tag -d v${version}`, { encoding: 'utf-8', stdio: 'pipe' }) console.log(`[提示] 已删除旧 tag: v${version}`) } catch { // tag 不存在,忽略错误 } // 添加新 tag execSync(`git tag v${version}`, { encoding: 'utf-8', stdio: 'inherit' }) return true } catch { return false } } // Git 推送(包含 tags) function gitPush(version) { try { // 确保终端处于正常模式,以便 git 可以进行交互 if (process.stdin.isTTY) { process.stdin.setRawMode(false) } // 先推送提交 execSync('git push', { encoding: 'utf-8', stdio: 'inherit' }) // 再单独推送 tag(强制更新远程 tag) execSync(`git push origin v${version} --force`, { encoding: 'utf-8', stdio: 'inherit' }) return true } catch { return false } } // 主循环 async function main() { showHeader() while (true) { const versionInput = await readInput('请输入新版本号: ') if (versionInput === '__EXIT__') { console.log('') console.log('再见!') process.exit(0) } if (!versionInput.trim()) { console.log('[错误] 版本号不能为空,请重新输入') console.log('') continue } if (!validateVersion(versionInput)) { console.log('[错误] 无效的版本号格式,请输入 x.y.z 格式或 patch/minor/major') console.log('') continue } const newVersion = getNewVersion(versionInput) if (!newVersion) { console.log('[错误] 无法计算新版本号,请检查输入') console.log('') continue } console.log('') console.log('+--------------------------------------------+') console.log('| 版本变更预览 |') console.log('+--------------------------------------------+') console.log(`| 当前版本: ${currentVersion}`) console.log(`| 新版本: ${newVersion}`) console.log('+--------------------------------------------+') console.log('') const confirm = await readYesNo('确认更新版本号?', false) if (!confirm) { console.log('') console.log('已取消,请重新输入版本号') console.log('') continue } // 检查版本号是否有变化 const versionChanged = currentVersion !== newVersion console.log('') console.log('正在更新版本号...') console.log('') if (!updateVersion(versionInput)) { console.log('') console.log('[错误] 版本号更新失败') continue } currentVersion = newVersion console.log('') console.log('============================================') console.log(` 版本号已成功更新到 ${newVersion}`) console.log('============================================') console.log('') if (versionChanged) { const gitChoice = await readYesNo('是否将修改提交到 Git?', true) if (gitChoice) { console.log('') console.log('正在提交到 Git...') if (gitCommit(newVersion)) { console.log('') console.log(`[成功] 已提交到 Git,提交消息: v${newVersion}`) } else { console.log('') console.log('[警告] Git 提交失败,可能没有变更或发生错误') } } else { console.log('') console.log('已跳过 Git 提交') } } else { console.log('[提示] 版本号未变化,跳过 Git 提交') } // 无论版本是否变化,都询问是否添加 tag const tagChoice = await readYesNo('是否添加 Git tag?', false) if (tagChoice) { console.log('') console.log('正在添加 tag...') if (gitTag(newVersion)) { console.log(`[成功] 已添加 tag: v${newVersion}`) // 询问是否推送 const pushChoice = await readYesNo('是否推送到远程仓库?', true) if (pushChoice) { console.log('') console.log('正在推送...') if (gitPush(newVersion)) { console.log('[成功] 已推送到远程仓库') } else { console.log('[警告] 推送失败') } } else { console.log('') console.log('已跳过推送') } } else { console.log('[警告] 添加 tag 失败,可能 tag 已存在') } } else { console.log('') console.log('已跳过添加 tag') } console.log('') const action = await waitForContinueOrExit() if (action === 'exit') { console.log('再见!') process.exit(0) } } } main().catch(console.error)