set-version.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import fs from 'fs'
  2. import path from 'path'
  3. import { fileURLToPath } from 'url'
  4. import { execSync } from 'child_process'
  5. import readline from 'readline'
  6. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  7. const projectRoot = path.dirname(__dirname)
  8. process.chdir(projectRoot)
  9. // 读取当前版本
  10. function getCurrentVersion() {
  11. const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
  12. return pkg.version
  13. }
  14. let currentVersion = getCurrentVersion()
  15. // 清屏并显示标题
  16. function showHeader() {
  17. console.log('')
  18. console.log('+--------------------------------------------+')
  19. console.log('| 版本号设置工具 |')
  20. console.log('| Version Bump Tool |')
  21. console.log('+--------------------------------------------+')
  22. console.log('')
  23. console.log(`当前版本: ${currentVersion}`)
  24. console.log('')
  25. console.log('支持的输入格式:')
  26. console.log(' - 具体版本号: 1.0.0, 2.1.3')
  27. console.log(' - patch: 补丁版本 +1 (如 0.0.6 -> 0.0.7)')
  28. console.log(' - minor: 次版本 +1 (如 0.0.6 -> 0.1.0)')
  29. console.log(' - major: 主版本 +1 (如 0.0.6 -> 1.0.0)')
  30. console.log(' - 按 q 键退出')
  31. console.log('')
  32. }
  33. // 单键读取
  34. function readKey() {
  35. return new Promise((resolve) => {
  36. const wasRaw = process.stdin.isRaw
  37. readline.emitKeypressEvents(process.stdin)
  38. if (process.stdin.isTTY) {
  39. process.stdin.setRawMode(true)
  40. }
  41. process.stdin.once('keypress', (ch, key) => {
  42. if (process.stdin.isTTY) {
  43. process.stdin.setRawMode(wasRaw)
  44. }
  45. resolve({ ch, key })
  46. })
  47. })
  48. }
  49. // 读取输入,支持单键 q 退出
  50. async function readInput(prompt) {
  51. process.stdout.write(prompt)
  52. let input = ''
  53. readline.emitKeypressEvents(process.stdin)
  54. if (process.stdin.isTTY) {
  55. process.stdin.setRawMode(true)
  56. }
  57. return new Promise((resolve) => {
  58. const onKeypress = (ch, key) => {
  59. if (key && key.name === 'return') {
  60. process.stdin.removeListener('keypress', onKeypress)
  61. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  62. console.log('')
  63. resolve(input)
  64. } else if (key && key.name === 'escape') {
  65. process.stdin.removeListener('keypress', onKeypress)
  66. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  67. console.log('')
  68. resolve('__EXIT__')
  69. } else if (key && key.name === 'backspace') {
  70. if (input.length > 0) {
  71. input = input.slice(0, -1)
  72. process.stdout.write('\b \b')
  73. }
  74. } else if (key && key.ctrl && key.name === 'c') {
  75. process.stdin.removeListener('keypress', onKeypress)
  76. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  77. console.log('')
  78. process.exit(0)
  79. } else if (ch && /[a-zA-Z0-9.\-]/.test(ch)) {
  80. input += ch
  81. process.stdout.write(ch)
  82. // 单独输入 q 立即退出
  83. if (input === 'q' || input === 'Q') {
  84. process.stdin.removeListener('keypress', onKeypress)
  85. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  86. console.log('')
  87. resolve('__EXIT__')
  88. }
  89. }
  90. }
  91. process.stdin.on('keypress', onKeypress)
  92. })
  93. }
  94. // 读取 Y/N
  95. async function readYesNo(prompt, defaultYes = true) {
  96. const hint = defaultYes ? 'Y/n' : 'y/N'
  97. process.stdout.write(`${prompt} (${hint}): `)
  98. readline.emitKeypressEvents(process.stdin)
  99. if (process.stdin.isTTY) {
  100. process.stdin.setRawMode(true)
  101. }
  102. return new Promise((resolve) => {
  103. const onKeypress = (ch, key) => {
  104. if (key && key.name === 'return') {
  105. process.stdin.removeListener('keypress', onKeypress)
  106. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  107. console.log(defaultYes ? 'y' : 'n')
  108. resolve(defaultYes)
  109. } else if (ch === 'y' || ch === 'Y') {
  110. process.stdin.removeListener('keypress', onKeypress)
  111. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  112. console.log('y')
  113. resolve(true)
  114. } else if (ch === 'n' || ch === 'N') {
  115. process.stdin.removeListener('keypress', onKeypress)
  116. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  117. console.log('n')
  118. resolve(false)
  119. } else if (key && key.ctrl && key.name === 'c') {
  120. process.stdin.removeListener('keypress', onKeypress)
  121. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  122. console.log('')
  123. process.exit(0)
  124. }
  125. }
  126. process.stdin.on('keypress', onKeypress)
  127. })
  128. }
  129. // 等待按键继续或退出
  130. async function waitForContinueOrExit() {
  131. process.stdout.write('按 Enter 继续修改,按 q 退出...')
  132. readline.emitKeypressEvents(process.stdin)
  133. if (process.stdin.isTTY) {
  134. process.stdin.setRawMode(true)
  135. }
  136. return new Promise((resolve) => {
  137. const onKeypress = (ch, key) => {
  138. if (key && key.name === 'return') {
  139. process.stdin.removeListener('keypress', onKeypress)
  140. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  141. console.log('\n')
  142. resolve('continue')
  143. } else if (ch === 'q' || ch === 'Q') {
  144. process.stdin.removeListener('keypress', onKeypress)
  145. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  146. console.log('\n')
  147. resolve('exit')
  148. } else if (key && key.ctrl && key.name === 'c') {
  149. process.stdin.removeListener('keypress', onKeypress)
  150. if (process.stdin.isTTY) process.stdin.setRawMode(false)
  151. console.log('')
  152. process.exit(0)
  153. }
  154. }
  155. process.stdin.on('keypress', onKeypress)
  156. })
  157. }
  158. // 验证版本号格式
  159. function validateVersion(input) {
  160. const keywords = ['patch', 'minor', 'major']
  161. if (keywords.includes(input.toLowerCase())) return true
  162. return /^\d+\.\d+\.\d+$/.test(input)
  163. }
  164. // 获取新版本号(dry-run)
  165. function getNewVersion(input) {
  166. try {
  167. const result = execSync(`node scripts/bump-version.mjs ${input} --dry-run`, {
  168. encoding: 'utf-8',
  169. stdio: ['pipe', 'pipe', 'pipe']
  170. })
  171. return result.trim()
  172. } catch {
  173. return null
  174. }
  175. }
  176. // 执行版本更新
  177. function updateVersion(input) {
  178. try {
  179. execSync(`node scripts/bump-version.mjs ${input}`, {
  180. encoding: 'utf-8',
  181. stdio: 'inherit'
  182. })
  183. return true
  184. } catch {
  185. return false
  186. }
  187. }
  188. // Git 提交
  189. function gitCommit(version) {
  190. try {
  191. // 注意:Cargo.lock 在 .gitignore 中,不需要添加
  192. execSync('git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml', {
  193. encoding: 'utf-8',
  194. stdio: 'inherit'
  195. })
  196. execSync(`git commit -m "v${version}"`, {
  197. encoding: 'utf-8',
  198. stdio: 'inherit'
  199. })
  200. return true
  201. } catch {
  202. return false
  203. }
  204. }
  205. // Git 添加 tag(如果已存在则先删除)
  206. function gitTag(version) {
  207. try {
  208. // 先尝试删除已存在的 tag
  209. try {
  210. execSync(`git tag -d v${version}`, {
  211. encoding: 'utf-8',
  212. stdio: 'pipe'
  213. })
  214. console.log(`[提示] 已删除旧 tag: v${version}`)
  215. } catch {
  216. // tag 不存在,忽略错误
  217. }
  218. // 添加新 tag
  219. execSync(`git tag v${version}`, {
  220. encoding: 'utf-8',
  221. stdio: 'inherit'
  222. })
  223. return true
  224. } catch {
  225. return false
  226. }
  227. }
  228. // Git 推送(包含 tags)
  229. function gitPush(version) {
  230. try {
  231. // 确保终端处于正常模式,以便 git 可以进行交互
  232. if (process.stdin.isTTY) {
  233. process.stdin.setRawMode(false)
  234. }
  235. // 先推送提交
  236. execSync('git push', {
  237. encoding: 'utf-8',
  238. stdio: 'inherit'
  239. })
  240. // 再单独推送 tag(强制更新远程 tag)
  241. execSync(`git push origin v${version} --force`, {
  242. encoding: 'utf-8',
  243. stdio: 'inherit'
  244. })
  245. return true
  246. } catch {
  247. return false
  248. }
  249. }
  250. // 主循环
  251. async function main() {
  252. showHeader()
  253. while (true) {
  254. const versionInput = await readInput('请输入新版本号: ')
  255. if (versionInput === '__EXIT__') {
  256. console.log('')
  257. console.log('再见!')
  258. process.exit(0)
  259. }
  260. if (!versionInput.trim()) {
  261. console.log('[错误] 版本号不能为空,请重新输入')
  262. console.log('')
  263. continue
  264. }
  265. if (!validateVersion(versionInput)) {
  266. console.log('[错误] 无效的版本号格式,请输入 x.y.z 格式或 patch/minor/major')
  267. console.log('')
  268. continue
  269. }
  270. const newVersion = getNewVersion(versionInput)
  271. if (!newVersion) {
  272. console.log('[错误] 无法计算新版本号,请检查输入')
  273. console.log('')
  274. continue
  275. }
  276. console.log('')
  277. console.log('+--------------------------------------------+')
  278. console.log('| 版本变更预览 |')
  279. console.log('+--------------------------------------------+')
  280. console.log(`| 当前版本: ${currentVersion}`)
  281. console.log(`| 新版本: ${newVersion}`)
  282. console.log('+--------------------------------------------+')
  283. console.log('')
  284. const confirm = await readYesNo('确认更新版本号?', false)
  285. if (!confirm) {
  286. console.log('')
  287. console.log('已取消,请重新输入版本号')
  288. console.log('')
  289. continue
  290. }
  291. // 检查版本号是否有变化
  292. const versionChanged = currentVersion !== newVersion
  293. console.log('')
  294. console.log('正在更新版本号...')
  295. console.log('')
  296. if (!updateVersion(versionInput)) {
  297. console.log('')
  298. console.log('[错误] 版本号更新失败')
  299. continue
  300. }
  301. currentVersion = newVersion
  302. console.log('')
  303. console.log('============================================')
  304. console.log(` 版本号已成功更新到 ${newVersion}`)
  305. console.log('============================================')
  306. console.log('')
  307. if (versionChanged) {
  308. const gitChoice = await readYesNo('是否将修改提交到 Git?', true)
  309. if (gitChoice) {
  310. console.log('')
  311. console.log('正在提交到 Git...')
  312. if (gitCommit(newVersion)) {
  313. console.log('')
  314. console.log(`[成功] 已提交到 Git,提交消息: v${newVersion}`)
  315. } else {
  316. console.log('')
  317. console.log('[警告] Git 提交失败,可能没有变更或发生错误')
  318. }
  319. } else {
  320. console.log('')
  321. console.log('已跳过 Git 提交')
  322. }
  323. } else {
  324. console.log('[提示] 版本号未变化,跳过 Git 提交')
  325. }
  326. // 无论版本是否变化,都询问是否添加 tag
  327. const tagChoice = await readYesNo('是否添加 Git tag?', false)
  328. if (tagChoice) {
  329. console.log('')
  330. console.log('正在添加 tag...')
  331. if (gitTag(newVersion)) {
  332. console.log(`[成功] 已添加 tag: v${newVersion}`)
  333. // 询问是否推送
  334. const pushChoice = await readYesNo('是否推送到远程仓库?', true)
  335. if (pushChoice) {
  336. console.log('')
  337. console.log('正在推送...')
  338. if (gitPush(newVersion)) {
  339. console.log('[成功] 已推送到远程仓库')
  340. } else {
  341. console.log('[警告] 推送失败')
  342. }
  343. } else {
  344. console.log('')
  345. console.log('已跳过推送')
  346. }
  347. } else {
  348. console.log('[警告] 添加 tag 失败,可能 tag 已存在')
  349. }
  350. } else {
  351. console.log('')
  352. console.log('已跳过添加 tag')
  353. }
  354. console.log('')
  355. const action = await waitForContinueOrExit()
  356. if (action === 'exit') {
  357. console.log('再见!')
  358. process.exit(0)
  359. }
  360. }
  361. }
  362. main().catch(console.error)