#!/usr/bin/env node import { spawnSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, '..', '..'); const npmCommand = process.platform === 'win32' ? 'cmd' : 'npm'; const hookInput = readHookInput(); if (hookInput && !isGitCommitCommand(extractShellCommand(hookInput))) { process.exit(0); } const validationSteps = [ { label: 'TypeScript typecheck', command: npmCommand, args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run typecheck'] : ['run', 'typecheck'], }, { label: 'Admin web typecheck', command: npmCommand, args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run admin-web:typecheck'] : ['run', 'admin-web:typecheck'], }, { label: 'Rust api-server compile check', command: 'cargo', args: ['check', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'], }, ]; for (const step of validationSteps) { const result = runStep(step); if (!result.ok) { const reason = `[codex-hook] 提交前编译检查失败:${step.label}。请修复编译错误后再提交。`; console.error(reason); if (hookInput) { console.log(JSON.stringify({ decision: 'block', reason })); process.exit(0); } process.exit(result.status ?? 1); } } console.error('[codex-hook] 提交前编译检查通过。'); function runStep(step) { console.error(`[codex-hook] ${step.label}`); const result = spawnSync(step.command, step.args, { cwd: repoRoot, shell: false, encoding: 'utf8', env: { ...process.env, NO_COLOR: process.env.NO_COLOR ?? '1', }, }); if (result.stdout) { process.stderr.write(result.stdout); } if (result.stderr) { process.stderr.write(result.stderr); } if (result.error) { console.error(`[codex-hook] ${step.label} 启动失败:${result.error.message}`); return { ok: false, status: 1 }; } if (result.signal) { console.error(`[codex-hook] ${step.label} 被信号终止:${result.signal}`); return { ok: false, status: 1 }; } return { ok: (result.status ?? 0) === 0, status: result.status ?? 1, }; } function readHookInput() { try { const rawInput = readFileSync(0, 'utf8').trim(); if (!rawInput) { return null; } return JSON.parse(rawInput); } catch { return null; } } function extractShellCommand(input) { const candidates = [ input?.tool_input?.command, input?.toolInput?.command, input?.tool_args?.command, input?.toolArgs?.command, input?.arguments?.command, input?.params?.command, input?.command, ]; const command = candidates.find(value => typeof value === 'string' && value.trim().length > 0); if (command) { return command; } const shellCommand = input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd; if (Array.isArray(shellCommand)) { return shellCommand.join(' '); } return ''; } function isGitCommitCommand(command) { return /(^|[;&|]\s*)git(?:\.exe)?\b[\s\S]{0,200}\bcommit\b/iu.test(command); }