111 lines
2.9 KiB
JavaScript
111 lines
2.9 KiB
JavaScript
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}`)}`);
|
|
}
|