| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- const fs = require('fs');
- const path = require('path');
- // PreToolUse hook 的输入结构,只关心本脚本实际会用到的字段。
- /**
- * @typedef {{
- * tool_name?: string,
- * tool_input?: {
- * command?: string,
- * args?: string[],
- * task?: {
- * command?: string,
- * args?: string[]
- * }
- * },
- * cwd?: string
- * }} HookPayload
- */
- // 读取 hook 通过 stdin 传入的 JSON 负载。
- function readStdin() {
- return new Promise((resolve, reject) => {
- let buffer = '';
- process.stdin.setEncoding('utf8');
- process.stdin.on('data', (chunk) => {
- buffer += chunk;
- });
- process.stdin.on('end', () => resolve(buffer));
- process.stdin.on('error', reject);
- });
- }
- // 通过命令行参数区分“普通脚本直跑”与“hook 回调”两种模式。
- function isDirectMode() {
- return process.argv.includes('--direct');
- }
- /**
- * 从不同工具的输入结构里提取实际命令,统一做字符串匹配。
- * @param {HookPayload['tool_input']} toolInput
- */
- function getToolCommand(toolInput) {
- if (!toolInput || typeof toolInput !== 'object') {
- return '';
- }
- if (typeof toolInput.command === 'string') {
- const args = Array.isArray(toolInput.args) ? toolInput.args.join(' ') : '';
- return [toolInput.command, args].filter(Boolean).join(' ');
- }
- if (toolInput.task && typeof toolInput.task.command === 'string') {
- const args = Array.isArray(toolInput.task.args) ? toolInput.task.args.join(' ') : '';
- return [toolInput.task.command, args].filter(Boolean).join(' ');
- }
- return '';
- }
- /**
- * 只在即将执行 npm run builder:win 时触发版本更新。
- * @param {HookPayload} payload
- */
- function shouldHandle(payload) {
- const toolName = payload.tool_name;
- const command = getToolCommand(payload.tool_input);
- if (toolName === 'run_in_terminal') {
- return command.includes('npm run builder:win');
- }
- if (toolName === 'create_and_run_task') {
- return command.includes('npm') && command.includes('builder:win');
- }
- return false;
- }
- /**
- * 将当前日期格式化为 VUE_APP_VERSION 使用的 YYYY.MM.DD 形式。
- * @param {Date} date
- */
- function formatDateForVersion(date) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- return `${year}.${month}.${day}`;
- }
- /**
- * 更新根目录 .env 中的 VUE_APP_VERSION;缺失文件或变量时返回失败。
- * @param {string} workspaceRoot
- * @param {string} version
- */
- function updateEnvVersion(workspaceRoot, version) {
- const envPath = path.join(workspaceRoot, '.env');
- if (!fs.existsSync(envPath)) {
- return {
- ok: false,
- reason: '.env 文件不存在,已阻止 npm run builder:win。',
- };
- }
- const original = fs.readFileSync(envPath, 'utf8');
- const eol = original.includes('\r\n') ? '\r\n' : '\n';
- const lines = original.split(/\r?\n/);
- let found = false;
- // 保留原有缩进和等号前后的空白,只替换版本值本身。
- const updatedLines = lines.map((line) => {
- if (!/^\s*VUE_APP_VERSION\s*=/.test(line)) {
- return line;
- }
- found = true;
- const prefixMatch = line.match(/^(\s*VUE_APP_VERSION\s*=\s*)/);
- const prefix = prefixMatch ? prefixMatch[1] : 'VUE_APP_VERSION = ';
- return `${prefix}'${version}'`;
- });
- if (!found) {
- return {
- ok: false,
- reason: '.env 中未找到 VUE_APP_VERSION,已阻止 npm run builder:win。',
- };
- }
- const updated = updatedLines.join(eol);
- if (updated !== original) {
- fs.writeFileSync(envPath, updated, 'utf8');
- }
- return {
- ok: true,
- envPath,
- version,
- };
- }
- /**
- * hook 放行时的统一输出;只有命中目标命令时才附带提示信息。
- * @param {string=} message
- */
- function allowWithMessage(message) {
- /** @type {{ hookSpecificOutput: { hookEventName: string, permissionDecision: 'allow', additionalContext?: string }, systemMessage?: string }} */
- const output = {
- hookSpecificOutput: {
- hookEventName: 'PreToolUse',
- permissionDecision: 'allow',
- },
- };
- if (message) {
- output.hookSpecificOutput.additionalContext = message;
- output.systemMessage = message;
- }
- process.stdout.write(JSON.stringify(output));
- }
- /**
- * hook 拒绝执行时的统一输出。
- * @param {string} reason
- */
- function denyWithReason(reason) {
- process.stdout.write(
- JSON.stringify({
- hookSpecificOutput: {
- hookEventName: 'PreToolUse',
- permissionDecision: 'deny',
- permissionDecisionReason: reason,
- },
- systemMessage: reason,
- }),
- );
- }
- /**
- * 直接运行模式:不给 hook 返回 JSON,只输出普通日志并用退出码表示成功或失败。
- * @param {string} workspaceRoot
- */
- function runDirectMode(workspaceRoot) {
- const version = formatDateForVersion(new Date());
- const result = updateEnvVersion(workspaceRoot, version);
- if (!result.ok) {
- process.stderr.write(`${result.reason || '更新 .env 失败。'}\n`);
- process.exitCode = 1;
- return;
- }
- process.stdout.write(`已在 .env 中将 VUE_APP_VERSION 更新为 ${version}。\n`);
- }
- async function main() {
- // npm script 直接调用时,不依赖 stdin 里的 hook 负载。
- if (isDirectMode()) {
- runDirectMode(process.cwd());
- return;
- }
- // hook 模式:读取 VS Code 传入的事件负载,再判断是否要拦截 builder:win。
- const rawInput = await readStdin();
- const payload = rawInput ? JSON.parse(rawInput) : {};
- if (!shouldHandle(payload)) {
- allowWithMessage();
- return;
- }
- const workspaceRoot = payload.cwd || process.cwd();
- const version = formatDateForVersion(new Date());
- const result = updateEnvVersion(workspaceRoot, version);
- if (!result.ok) {
- denyWithReason(result.reason || '更新 .env 失败,已阻止 npm run builder:win。');
- return;
- }
- allowWithMessage(`已在 .env 中将 VUE_APP_VERSION 更新为 ${version},继续执行 npm run builder:win。`);
- }
- main().catch((error) => {
- denyWithReason(`更新 .env 失败,已阻止 npm run builder:win。原因: ${error.message}`);
- });
|