update-env-version-on-builder-win.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. const fs = require('fs');
  2. const path = require('path');
  3. // PreToolUse hook 的输入结构,只关心本脚本实际会用到的字段。
  4. /**
  5. * @typedef {{
  6. * tool_name?: string,
  7. * tool_input?: {
  8. * command?: string,
  9. * args?: string[],
  10. * task?: {
  11. * command?: string,
  12. * args?: string[]
  13. * }
  14. * },
  15. * cwd?: string
  16. * }} HookPayload
  17. */
  18. // 读取 hook 通过 stdin 传入的 JSON 负载。
  19. function readStdin() {
  20. return new Promise((resolve, reject) => {
  21. let buffer = '';
  22. process.stdin.setEncoding('utf8');
  23. process.stdin.on('data', (chunk) => {
  24. buffer += chunk;
  25. });
  26. process.stdin.on('end', () => resolve(buffer));
  27. process.stdin.on('error', reject);
  28. });
  29. }
  30. // 通过命令行参数区分“普通脚本直跑”与“hook 回调”两种模式。
  31. function isDirectMode() {
  32. return process.argv.includes('--direct');
  33. }
  34. /**
  35. * 从不同工具的输入结构里提取实际命令,统一做字符串匹配。
  36. * @param {HookPayload['tool_input']} toolInput
  37. */
  38. function getToolCommand(toolInput) {
  39. if (!toolInput || typeof toolInput !== 'object') {
  40. return '';
  41. }
  42. if (typeof toolInput.command === 'string') {
  43. const args = Array.isArray(toolInput.args) ? toolInput.args.join(' ') : '';
  44. return [toolInput.command, args].filter(Boolean).join(' ');
  45. }
  46. if (toolInput.task && typeof toolInput.task.command === 'string') {
  47. const args = Array.isArray(toolInput.task.args) ? toolInput.task.args.join(' ') : '';
  48. return [toolInput.task.command, args].filter(Boolean).join(' ');
  49. }
  50. return '';
  51. }
  52. /**
  53. * 只在即将执行 npm run builder:win 时触发版本更新。
  54. * @param {HookPayload} payload
  55. */
  56. function shouldHandle(payload) {
  57. const toolName = payload.tool_name;
  58. const command = getToolCommand(payload.tool_input);
  59. if (toolName === 'run_in_terminal') {
  60. return command.includes('npm run builder:win');
  61. }
  62. if (toolName === 'create_and_run_task') {
  63. return command.includes('npm') && command.includes('builder:win');
  64. }
  65. return false;
  66. }
  67. /**
  68. * 将当前日期格式化为 VUE_APP_VERSION 使用的 YYYY.MM.DD 形式。
  69. * @param {Date} date
  70. */
  71. function formatDateForVersion(date) {
  72. const year = date.getFullYear();
  73. const month = String(date.getMonth() + 1).padStart(2, '0');
  74. const day = String(date.getDate()).padStart(2, '0');
  75. return `${year}.${month}.${day}`;
  76. }
  77. /**
  78. * 更新根目录 .env 中的 VUE_APP_VERSION;缺失文件或变量时返回失败。
  79. * @param {string} workspaceRoot
  80. * @param {string} version
  81. */
  82. function updateEnvVersion(workspaceRoot, version) {
  83. const envPath = path.join(workspaceRoot, '.env');
  84. if (!fs.existsSync(envPath)) {
  85. return {
  86. ok: false,
  87. reason: '.env 文件不存在,已阻止 npm run builder:win。',
  88. };
  89. }
  90. const original = fs.readFileSync(envPath, 'utf8');
  91. const eol = original.includes('\r\n') ? '\r\n' : '\n';
  92. const lines = original.split(/\r?\n/);
  93. let found = false;
  94. // 保留原有缩进和等号前后的空白,只替换版本值本身。
  95. const updatedLines = lines.map((line) => {
  96. if (!/^\s*VUE_APP_VERSION\s*=/.test(line)) {
  97. return line;
  98. }
  99. found = true;
  100. const prefixMatch = line.match(/^(\s*VUE_APP_VERSION\s*=\s*)/);
  101. const prefix = prefixMatch ? prefixMatch[1] : 'VUE_APP_VERSION = ';
  102. return `${prefix}'${version}'`;
  103. });
  104. if (!found) {
  105. return {
  106. ok: false,
  107. reason: '.env 中未找到 VUE_APP_VERSION,已阻止 npm run builder:win。',
  108. };
  109. }
  110. const updated = updatedLines.join(eol);
  111. if (updated !== original) {
  112. fs.writeFileSync(envPath, updated, 'utf8');
  113. }
  114. return {
  115. ok: true,
  116. envPath,
  117. version,
  118. };
  119. }
  120. /**
  121. * hook 放行时的统一输出;只有命中目标命令时才附带提示信息。
  122. * @param {string=} message
  123. */
  124. function allowWithMessage(message) {
  125. /** @type {{ hookSpecificOutput: { hookEventName: string, permissionDecision: 'allow', additionalContext?: string }, systemMessage?: string }} */
  126. const output = {
  127. hookSpecificOutput: {
  128. hookEventName: 'PreToolUse',
  129. permissionDecision: 'allow',
  130. },
  131. };
  132. if (message) {
  133. output.hookSpecificOutput.additionalContext = message;
  134. output.systemMessage = message;
  135. }
  136. process.stdout.write(JSON.stringify(output));
  137. }
  138. /**
  139. * hook 拒绝执行时的统一输出。
  140. * @param {string} reason
  141. */
  142. function denyWithReason(reason) {
  143. process.stdout.write(
  144. JSON.stringify({
  145. hookSpecificOutput: {
  146. hookEventName: 'PreToolUse',
  147. permissionDecision: 'deny',
  148. permissionDecisionReason: reason,
  149. },
  150. systemMessage: reason,
  151. }),
  152. );
  153. }
  154. /**
  155. * 直接运行模式:不给 hook 返回 JSON,只输出普通日志并用退出码表示成功或失败。
  156. * @param {string} workspaceRoot
  157. */
  158. function runDirectMode(workspaceRoot) {
  159. const version = formatDateForVersion(new Date());
  160. const result = updateEnvVersion(workspaceRoot, version);
  161. if (!result.ok) {
  162. process.stderr.write(`${result.reason || '更新 .env 失败。'}\n`);
  163. process.exitCode = 1;
  164. return;
  165. }
  166. process.stdout.write(`已在 .env 中将 VUE_APP_VERSION 更新为 ${version}。\n`);
  167. }
  168. async function main() {
  169. // npm script 直接调用时,不依赖 stdin 里的 hook 负载。
  170. if (isDirectMode()) {
  171. runDirectMode(process.cwd());
  172. return;
  173. }
  174. // hook 模式:读取 VS Code 传入的事件负载,再判断是否要拦截 builder:win。
  175. const rawInput = await readStdin();
  176. const payload = rawInput ? JSON.parse(rawInput) : {};
  177. if (!shouldHandle(payload)) {
  178. allowWithMessage();
  179. return;
  180. }
  181. const workspaceRoot = payload.cwd || process.cwd();
  182. const version = formatDateForVersion(new Date());
  183. const result = updateEnvVersion(workspaceRoot, version);
  184. if (!result.ok) {
  185. denyWithReason(result.reason || '更新 .env 失败,已阻止 npm run builder:win。');
  186. return;
  187. }
  188. allowWithMessage(`已在 .env 中将 VUE_APP_VERSION 更新为 ${version},继续执行 npm run builder:win。`);
  189. }
  190. main().catch((error) => {
  191. denyWithReason(`更新 .env 失败,已阻止 npm run builder:win。原因: ${error.message}`);
  192. });