#!/usr/bin/env node
import { existsSync, readFileSync, statSync } from "node:fs";
import { isAbsolute, relative, resolve } from "node:path";
import { spawnSync } from "node:child_process";
const root = git(["rev-parse", "--show-toplevel"]) || process.cwd();
process.chdir(root);
const stagedOnly = process.argv.includes("--staged");
const hookFiles = await filesFromHookInput();
const files = hookFiles.length > 0 ? hookFiles : filesFromGitChanges(stagedOnly);
const existingFiles = unique(files)
.map((path) => normalizeRepoPath(path))
.filter((path) => path && existsSync(path) && statSync(path).isFile())
.filter((path) => !isGeneratedOutput(path));
const biomeFiles = existingFiles.filter(isBiomeFile);
const rustFiles = existingFiles.filter((path) => path.endsWith(".rs"));
if (biomeFiles.length > 0) {
run("pnpm", [
"biome",
"format",
"--write",
"--files-ignore-unknown=true",
"--no-errors-on-unmatched",
...biomeFiles,
]);
}
if (rustFiles.length > 0) {
run("rustfmt", rustFiles);
}
async function filesFromHookInput() {
if (process.stdin.isTTY) return [];
const raw = readFileSync(0, "utf8");
if (!raw.trim()) return [];
const files = [];
try {
const payload = JSON.parse(raw);
collectPathValues(payload, files);
} catch {
}
for (const match of raw.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)) {
files.push(match[1].trim());
}
return files;
}
function collectPathValues(value, files) {
if (!value || typeof value !== "object") return;
for (const [key, nested] of Object.entries(value)) {
if (
typeof nested === "string" &&
(key === "file_path" || key === "path" || key === "filename")
) {
files.push(nested);
continue;
}
collectPathValues(nested, files);
}
}
function filesFromGitChanges(stagedOnly) {
if (stagedOnly) {
return gitLines(["diff", "--name-only", "--cached", "--diff-filter=ACMR"]);
}
return unique([
...gitLines(["diff", "--name-only", "--diff-filter=ACMR"]),
...gitLines(["diff", "--name-only", "--cached", "--diff-filter=ACMR"]),
...gitLines(["ls-files", "--others", "--exclude-standard"]),
]);
}
function normalizeRepoPath(path) {
const absolute = isAbsolute(path) ? path : resolve(root, path);
const relativePath = relative(root, absolute);
if (!relativePath || relativePath.startsWith("..")) return null;
return relativePath;
}
function isGeneratedOutput(path) {
return (
/^engine\/sdks\/[^/]+\/api-[^/]+\//.test(path) ||
/^engine\/sdks\/typescript\/(?:envoy-protocol|runner-protocol)\//.test(
path,
) ||
/^rivetkit-typescript\/packages\/engine-runner-protocol\//.test(path) ||
/^rivetkit-typescript\/packages\/rivetkit\/src\/common\/bare\/generated\//.test(
path,
) ||
path === "rivetkit-typescript/packages/rivetkit-napi/index.d.ts" ||
path === "rivetkit-typescript/packages/rivetkit-wasm/index.d.ts"
);
}
function isBiomeFile(path) {
return /\.(?:ts|tsx|js|jsx|mjs|cjs|css|json|jsonc)$/.test(path);
}
function unique(values) {
return [...new Set(values.filter(Boolean))];
}
function git(args) {
const result = spawnSync("git", args, { encoding: "utf8" });
return result.status === 0 ? result.stdout.trim() : null;
}
function gitLines(args) {
return (git(args) || "").split("\n").filter(Boolean);
}
function run(command, args) {
const result = spawnSync(command, args, { stdio: "inherit" });
if (result.status !== 0) process.exit(result.status ?? 1);
}