import { existsSync, readFileSync, rmSync, statSync } from 'node:fs'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { tmpdir } from 'node:os'; const readInput = () => { try { const rawInput = readFileSync(0, 'utf8').trim(); return rawInput ? JSON.parse(rawInput) : {}; } catch { return {}; } }; const runGit = (args) => { const result = spawnSync('git', args, { encoding: 'utf8' }); return result.status === 0 ? result.stdout.split('\n').filter(Boolean) : []; }; const runCommand = (command, args) => spawnSync(command, args, { encoding: 'utf8', shell: false, }); const getStatePath = (input) => { const turnId = String(input.turn_id ?? 'unknown').replace(/[^a-zA-Z0-9_.-]/g, '_'); const cwdHash = createHash('sha256').update(process.cwd()).digest('hex').slice(0, 16); return join(tmpdir(), 'codex-fastboard-hooks', cwdHash, `${turnId}.json`); }; const isTypeScriptSource = (file) => /^src\/.+\.tsx?$/.test(file); const getDirtyTypeScriptFiles = () => { const files = [ ...runGit(['diff', '--name-only', '--diff-filter=ACMR']), ...runGit(['diff', '--cached', '--name-only', '--diff-filter=ACMR']), ...runGit(['ls-files', '--others', '--exclude-standard']), ]; return [...new Set(files)].filter(isTypeScriptSource).sort(); }; const getFileState = (file) => { try { const stat = statSync(file); return { mtimeMs: stat.mtimeMs, size: stat.size }; } catch { return null; } }; const hasChangedSinceSnapshot = (beforeState, file) => { const before = beforeState[file]; const current = getFileState(file); if (!current) { return false; } return !before || before.mtimeMs !== current.mtimeMs || before.size !== current.size; }; const firstLines = (value, maxLines = 30) => value .split('\n') .filter(Boolean) .slice(0, maxLines) .join('\n'); const report = (message) => { process.stdout.write( JSON.stringify({ continue: true, systemMessage: message, }), ); }; const input = readInput(); const statePath = getStatePath(input); const beforeState = existsSync(statePath) ? JSON.parse(readFileSync(statePath, 'utf8')) : {}; const files = getDirtyTypeScriptFiles().filter((file) => hasChangedSinceSnapshot(beforeState, file)); if (existsSync(statePath)) { rmSync(statePath, { force: true }); } if (files.length === 0) { process.exit(0); } const prettier = runCommand('npx', ['prettier', '--write', ...files]); if (prettier.status !== 0) { report(`Prettier hook failed:\n${firstLines(`${prettier.stdout}\n${prettier.stderr}`)}`); process.exit(0); } const eslint = runCommand('npx', [ 'eslint', '--no-error-on-unmatched-pattern', '--max-warnings=0', ...files, ]); if (eslint.status !== 0) { report(`ESLint hook found issues:\n${firstLines(`${eslint.stdout}\n${eslint.stderr}`)}`); }