set-version.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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. execSync('git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml src-tauri/Cargo.lock', {
  192. encoding: 'utf-8',
  193. stdio: 'inherit'
  194. })
  195. execSync(`git commit -m "v${version}"`, {
  196. encoding: 'utf-8',
  197. stdio: 'inherit'
  198. })
  199. return true
  200. } catch {
  201. return false
  202. }
  203. }
  204. // Git 添加 tag(如果已存在则先删除)
  205. function gitTag(version) {
  206. try {
  207. // 先尝试删除已存在的 tag
  208. try {
  209. execSync(`git tag -d v${version}`, {
  210. encoding: 'utf-8',
  211. stdio: 'pipe'
  212. })
  213. console.log(`[提示] 已删除旧 tag: v${version}`)
  214. } catch {
  215. // tag 不存在,忽略错误
  216. }
  217. // 添加新 tag
  218. execSync(`git tag v${version}`, {
  219. encoding: 'utf-8',
  220. stdio: 'inherit'
  221. })
  222. return true
  223. } catch {
  224. return false
  225. }
  226. }
  227. // Git 推送(包含 tags)
  228. function gitPush() {
  229. try {
  230. // 确保终端处于正常模式,以便 git 可以进行交互
  231. if (process.stdin.isTTY) {
  232. process.stdin.setRawMode(false)
  233. }
  234. execSync('git push --follow-tags', {
  235. encoding: 'utf-8',
  236. stdio: 'inherit'
  237. })
  238. return true
  239. } catch {
  240. return false
  241. }
  242. }
  243. // 主循环
  244. async function main() {
  245. showHeader()
  246. while (true) {
  247. const versionInput = await readInput('请输入新版本号: ')
  248. if (versionInput === '__EXIT__') {
  249. console.log('')
  250. console.log('再见!')
  251. process.exit(0)
  252. }
  253. if (!versionInput.trim()) {
  254. console.log('[错误] 版本号不能为空,请重新输入')
  255. console.log('')
  256. continue
  257. }
  258. if (!validateVersion(versionInput)) {
  259. console.log('[错误] 无效的版本号格式,请输入 x.y.z 格式或 patch/minor/major')
  260. console.log('')
  261. continue
  262. }
  263. const newVersion = getNewVersion(versionInput)
  264. if (!newVersion) {
  265. console.log('[错误] 无法计算新版本号,请检查输入')
  266. console.log('')
  267. continue
  268. }
  269. console.log('')
  270. console.log('+--------------------------------------------+')
  271. console.log('| 版本变更预览 |')
  272. console.log('+--------------------------------------------+')
  273. console.log(`| 当前版本: ${currentVersion}`)
  274. console.log(`| 新版本: ${newVersion}`)
  275. console.log('+--------------------------------------------+')
  276. console.log('')
  277. const confirm = await readYesNo('确认更新版本号?', false)
  278. if (!confirm) {
  279. console.log('')
  280. console.log('已取消,请重新输入版本号')
  281. console.log('')
  282. continue
  283. }
  284. // 检查版本号是否有变化
  285. const versionChanged = currentVersion !== newVersion
  286. console.log('')
  287. console.log('正在更新版本号...')
  288. console.log('')
  289. if (!updateVersion(versionInput)) {
  290. console.log('')
  291. console.log('[错误] 版本号更新失败')
  292. continue
  293. }
  294. currentVersion = newVersion
  295. console.log('')
  296. console.log('============================================')
  297. console.log(` 版本号已成功更新到 ${newVersion}`)
  298. console.log('============================================')
  299. console.log('')
  300. if (versionChanged) {
  301. const gitChoice = await readYesNo('是否将修改提交到 Git?', true)
  302. if (gitChoice) {
  303. console.log('')
  304. console.log('正在提交到 Git...')
  305. if (gitCommit(newVersion)) {
  306. console.log('')
  307. console.log(`[成功] 已提交到 Git,提交消息: v${newVersion}`)
  308. } else {
  309. console.log('')
  310. console.log('[警告] Git 提交失败,可能没有变更或发生错误')
  311. }
  312. } else {
  313. console.log('')
  314. console.log('已跳过 Git 提交')
  315. }
  316. } else {
  317. console.log('[提示] 版本号未变化,跳过 Git 提交')
  318. }
  319. // 无论版本是否变化,都询问是否添加 tag
  320. const tagChoice = await readYesNo('是否添加 Git tag?', false)
  321. if (tagChoice) {
  322. console.log('')
  323. console.log('正在添加 tag...')
  324. if (gitTag(newVersion)) {
  325. console.log(`[成功] 已添加 tag: v${newVersion}`)
  326. // 询问是否推送
  327. const pushChoice = await readYesNo('是否推送到远程仓库?', true)
  328. if (pushChoice) {
  329. console.log('')
  330. console.log('正在推送...')
  331. if (gitPush()) {
  332. console.log('[成功] 已推送到远程仓库')
  333. } else {
  334. console.log('[警告] 推送失败')
  335. }
  336. } else {
  337. console.log('')
  338. console.log('已跳过推送')
  339. }
  340. } else {
  341. console.log('[警告] 添加 tag 失败,可能 tag 已存在')
  342. }
  343. } else {
  344. console.log('')
  345. console.log('已跳过添加 tag')
  346. }
  347. console.log('')
  348. const action = await waitForContinueOrExit()
  349. if (action === 'exit') {
  350. console.log('再见!')
  351. process.exit(0)
  352. }
  353. }
  354. }
  355. main().catch(console.error)