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}`); });