From 755afe148368e3a7137587cb41fea9ae62055dc2 Mon Sep 17 00:00:00 2001 From: zly Date: Thu, 9 Apr 2026 09:36:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20=E4=BC=98=E5=8C=96git=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=EF=BC=8C=E4=BD=BF=E7=94=A8prompts=E6=9B=BF=E6=8D=A2re?= =?UTF-8?q?adline=EF=BC=8C=E6=8F=90=E5=8D=87cli=E7=9A=84=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 23 ++++++ scripts/merge-to-production.js | 133 +++++++++++++++------------------ 3 files changed, 84 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index f31d1a9..2bbc846 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "postcss-less": "^5.0.0", "prettier": "^2.4.1", "pretty-quick": "^3.1.2", + "prompts": "^2.4.2", "stylelint": "^14.1.0", "stylelint-config-prettier": "^9.0.3", "stylelint-config-standard": "^24.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ccb0259..d4df3db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,6 +192,9 @@ importers: pretty-quick: specifier: ^3.1.2 version: 3.3.1(prettier@2.8.8) + prompts: + specifier: ^2.4.2 + version: 2.4.2 stylelint: specifier: ^14.1.0 version: 14.16.1 @@ -3312,6 +3315,10 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -4021,6 +4028,10 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -4475,6 +4486,9 @@ packages: simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + size-sensor@1.0.2: resolution: {integrity: sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==} @@ -9351,6 +9365,8 @@ snapshots: kind-of@6.0.3: {} + kleur@3.0.3: {} + kleur@4.1.5: {} klona@2.0.6: {} @@ -10375,6 +10391,11 @@ snapshots: progress@2.0.3: {} + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -10945,6 +10966,8 @@ snapshots: dependencies: is-arrayish: 0.3.4 + sisteransi@1.0.5: {} + size-sensor@1.0.2: {} skmeans@0.9.7: {} diff --git a/scripts/merge-to-production.js b/scripts/merge-to-production.js index ae0f986..58616aa 100644 --- a/scripts/merge-to-production.js +++ b/scripts/merge-to-production.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -const readline = require('readline'); const { spawnSync } = require('child_process'); +const prompts = require('prompts'); const COLORS = { red: '\x1b[31m', @@ -19,6 +19,13 @@ class MergeConflictError extends Error { } } +class PromptAbortedError extends Error { + constructor() { + super('操作已取消'); + this.name = 'PromptAbortedError'; + } +} + function log(message, color = 'reset') { console.log(`${COLORS[color]}${message}${COLORS.reset}`); } @@ -53,24 +60,6 @@ function git(args, options = {}) { return runCommand('git', args, options); } -function createPrompt() { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return { - ask(question) { - return new Promise(resolve => { - rl.question(question, answer => resolve(answer.trim())); - }); - }, - close() { - rl.close(); - } - }; -} - function getCurrentBranch() { return git(['branch', '--show-current'], { silent: true }).trim(); } @@ -178,68 +167,67 @@ function mergeBranch(sourceBranch, targetBranch, state) { log('合并成功\n', 'green'); } -function printMenu(title, options) { - log(`\n${title}`, 'cyan'); - options.forEach((option, index) => { - console.log(` ${index + 1}. ${option.label}`); - }); - console.log(''); -} - -async function selectOption(prompt, title, options) { - while (true) { - printMenu(title, options); - const answer = await prompt.ask('请输入编号并回车: '); - const index = Number(answer); - - if (Number.isInteger(index) && index >= 1 && index <= options.length) { - return options[index - 1]; +async function promptValue(config) { + const response = await prompts(config, { + onCancel() { + throw new PromptAbortedError(); } + }); - log('输入无效,请重新选择。\n', 'red'); - } + return response[config.name]; } -async function selectBranch(prompt, title, branches, currentBranch) { - const options = branches.map(branchName => ({ - value: branchName, - label: branchName === currentBranch ? `${branchName} (当前分支)` : branchName - })); - - const selected = await selectOption(prompt, title, options); - return selected.value; +async function selectOption(title, options) { + log('', 'reset'); + + return promptValue({ + type: 'select', + name: 'value', + message: title, + hint: '使用上下方向键选择,回车确认', + choices: options.map(option => ({ + title: option.label, + value: option.value, + description: option.description + })) + }); } -async function confirm(prompt, message) { - while (true) { - const answer = (await prompt.ask(`${message} (y/n): `)).toLowerCase(); - - if (answer === 'y' || answer === 'yes') { - return true; - } +async function selectBranch(title, branches, currentBranch) { + return selectOption( + title, + branches.map(branchName => ({ + value: branchName, + label: branchName === currentBranch ? `${branchName} (当前分支)` : branchName + })) + ); +} - if (answer === 'n' || answer === 'no') { - return false; - } +async function confirm(message) { + log('', 'reset'); - log('请输入 y 或 n。\n', 'red'); - } + return promptValue({ + type: 'confirm', + name: 'confirmed', + message, + initial: true + }); } -async function runSingleBranchMerge(prompt, state) { +async function runSingleBranchMerge(state) { const branches = getLocalBranches(); if (branches.length < 2) { throw new Error('本地分支数量不足,无法执行单分支合并'); } - const sourceBranch = await selectBranch(prompt, '请选择来源分支', branches, state.currentBranch); + const sourceBranch = await selectBranch('请选择来源分支', branches, state.currentBranch); const targetCandidates = branches.filter(branchName => branchName !== sourceBranch); - const targetBranch = await selectBranch(prompt, '请选择目标分支', targetCandidates, state.currentBranch); + const targetBranch = await selectBranch('请选择目标分支', targetCandidates, state.currentBranch); log(`\n将执行: ${sourceBranch} -> ${targetBranch}\n`, 'cyan'); - const accepted = await confirm(prompt, '确认开始合并吗'); + const accepted = await confirm('确认开始合并吗'); if (!accepted) { log('已取消操作', 'yellow'); return; @@ -257,14 +245,14 @@ async function runSingleBranchMerge(prompt, state) { log('========================================\n', 'green'); } -async function runDeployMerge(prompt, state) { +async function runDeployMerge(state) { if (state.currentBranch === 'production') { throw new Error('当前分支为 production,不能执行部署合并'); } log(`\n将执行部署合并: ${state.currentBranch} -> master -> production\n`, 'cyan'); - const accepted = await confirm(prompt, '确认开始部署合并吗'); + const accepted = await confirm('确认开始部署合并吗'); if (!accepted) { log('已取消操作', 'yellow'); return; @@ -295,7 +283,6 @@ async function runDeployMerge(prompt, state) { } async function run() { - const prompt = createPrompt(); const state = { currentBranch: '', hasChanges: false, @@ -316,27 +303,27 @@ async function run() { log(`当前分支: ${state.currentBranch}\n`, 'cyan'); - const action = await selectOption(prompt, '请选择操作类型', [ - { label: '单分支合并', value: 'single' }, - { label: '部署合并(当前分支 -> master -> production)', value: 'deploy' } + const action = await selectOption('请选择操作类型', [ + { label: '单分支合并', value: 'single', description: '手动选择来源分支和目标分支' }, + { label: '部署合并', value: 'deploy', description: '将当前分支依次合并到 master 和 production' } ]); - if (action.value === 'single') { - await runSingleBranchMerge(prompt, state); + if (action === 'single') { + await runSingleBranchMerge(state); } else { - await runDeployMerge(prompt, state); + await runDeployMerge(state); } } catch (error) { if (error instanceof MergeConflictError) { printConflictGuidance(error.branchName); + } else if (error instanceof PromptAbortedError) { + log('\n已取消操作\n', 'yellow'); } else { log(`\n错误: ${error.message}\n`, 'red'); } - process.exitCode = 1; + process.exitCode = error instanceof PromptAbortedError ? 0 : 1; } finally { - prompt.close(); - if (state.shouldRestoreWorkspace) { restoreWorkspace(state); }