mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
208 lines
7.2 KiB
JavaScript
Executable File
208 lines
7.2 KiB
JavaScript
Executable File
#!/usr/bin/env bun
|
|
import { basename, extname } from "path";
|
|
|
|
const input = await Bun.stdin.json();
|
|
|
|
const toolName = input.tool_name;
|
|
const toolInput = input.tool_input || {};
|
|
const command = toolInput.command || "";
|
|
const timeout = toolInput.timeout;
|
|
const cwd = input.cwd || "";
|
|
|
|
// Get environment variables from the hook context
|
|
// Note: We check process.env directly as env vars are inherited
|
|
let useSystemBun = process.env.USE_SYSTEM_BUN;
|
|
|
|
if (toolName !== "Bash" || !command) {
|
|
process.exit(0);
|
|
}
|
|
|
|
function denyWithReason(reason) {
|
|
const output = {
|
|
hookSpecificOutput: {
|
|
hookEventName: "PreToolUse",
|
|
permissionDecision: "deny",
|
|
permissionDecisionReason: reason,
|
|
},
|
|
};
|
|
console.log(JSON.stringify(output));
|
|
process.exit(0);
|
|
}
|
|
|
|
// Parse the command to extract argv0 and positional args
|
|
let tokens;
|
|
try {
|
|
// Simple shell parsing - split on spaces but respect quotes (both single and double)
|
|
tokens = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map(t => t.replace(/^['"]|['"]$/g, "")) || [];
|
|
} catch {
|
|
process.exit(0);
|
|
}
|
|
|
|
if (tokens.length === 0) {
|
|
process.exit(0);
|
|
}
|
|
|
|
// Strip inline environment variable assignments (e.g., FOO=1 bun test)
|
|
const inlineEnv = new Map();
|
|
let commandStart = 0;
|
|
while (
|
|
commandStart < tokens.length &&
|
|
/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[commandStart]) &&
|
|
!tokens[commandStart].includes("/")
|
|
) {
|
|
const [name, value = ""] = tokens[commandStart].split("=", 2);
|
|
inlineEnv.set(name, value);
|
|
commandStart++;
|
|
}
|
|
if (commandStart >= tokens.length) {
|
|
process.exit(0);
|
|
}
|
|
tokens = tokens.slice(commandStart);
|
|
useSystemBun = inlineEnv.get("USE_SYSTEM_BUN") ?? useSystemBun;
|
|
|
|
// Get the executable name (argv0)
|
|
const argv0 = basename(tokens[0], extname(tokens[0]));
|
|
|
|
// Check if it's zig or zig.exe
|
|
if (argv0 === "zig") {
|
|
// Filter out flags (starting with -) to get positional arguments
|
|
const positionalArgs = tokens.slice(1).filter(arg => !arg.startsWith("-"));
|
|
|
|
// Check if the positional args contain "build" followed by "obj"
|
|
if (positionalArgs.length >= 2 && positionalArgs[0] === "build" && positionalArgs[1] === "obj") {
|
|
denyWithReason("error: Use `bun bd` to build Bun and wait patiently");
|
|
}
|
|
}
|
|
|
|
// Check if argv0 is timeout and the command is "bun bd"
|
|
if (argv0 === "timeout") {
|
|
// Find the actual command after timeout and its arguments
|
|
const timeoutArgEndIndex = tokens.slice(1).findIndex(t => !t.startsWith("-") && !/^\d/.test(t));
|
|
if (timeoutArgEndIndex === -1) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const actualCommandIndex = timeoutArgEndIndex + 1;
|
|
if (actualCommandIndex >= tokens.length) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const actualCommand = basename(tokens[actualCommandIndex]);
|
|
const restArgs = tokens.slice(actualCommandIndex + 1);
|
|
|
|
// Check if it's "bun bd" or "bun-debug bd" without other positional args
|
|
if (actualCommand === "bun" || actualCommand.includes("bun-debug")) {
|
|
// Claude is a sneaky fucker
|
|
let positionalArgs = restArgs.filter(arg => !arg.startsWith("-"));
|
|
const redirectStderrToStdoutIndex = positionalArgs.findIndex(arg => arg === "2>&1");
|
|
if (redirectStderrToStdoutIndex !== -1) {
|
|
positionalArgs.splice(redirectStderrToStdoutIndex, 1);
|
|
}
|
|
const redirectStdoutToStderrIndex = positionalArgs.findIndex(arg => arg === "1>&2");
|
|
if (redirectStdoutToStderrIndex !== -1) {
|
|
positionalArgs.splice(redirectStdoutToStderrIndex, 1);
|
|
}
|
|
|
|
const redirectToFileIndex = positionalArgs.findIndex(arg => arg === ">");
|
|
if (redirectToFileIndex !== -1) {
|
|
positionalArgs.splice(redirectToFileIndex, 2);
|
|
}
|
|
|
|
const redirectToFileAppendIndex = positionalArgs.findIndex(arg => arg === ">>");
|
|
if (redirectToFileAppendIndex !== -1) {
|
|
positionalArgs.splice(redirectToFileAppendIndex, 2);
|
|
}
|
|
|
|
const redirectTOFileInlineIndex = positionalArgs.findIndex(arg => arg.startsWith(">"));
|
|
if (redirectTOFileInlineIndex !== -1) {
|
|
positionalArgs.splice(redirectTOFileInlineIndex, 1);
|
|
}
|
|
|
|
const pipeIndex = positionalArgs.findIndex(arg => arg === "|");
|
|
if (pipeIndex !== -1) {
|
|
positionalArgs = positionalArgs.slice(0, pipeIndex);
|
|
}
|
|
|
|
positionalArgs = positionalArgs.map(arg => arg.trim()).filter(Boolean);
|
|
|
|
if (positionalArgs.length === 1 && positionalArgs[0] === "bd") {
|
|
denyWithReason("error: Run `bun bd` without a timeout");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if command is "bun .* test" or "bun-debug test" with -u/--update-snapshots AND -t/--test-name-pattern
|
|
if (argv0 === "bun" || argv0.includes("bun-debug")) {
|
|
const allArgs = tokens.slice(1);
|
|
|
|
// Check if "test" is in positional args or "bd" followed by "test"
|
|
const positionalArgs = allArgs.filter(arg => !arg.startsWith("-"));
|
|
const hasTest = positionalArgs.includes("test") || (positionalArgs[0] === "bd" && positionalArgs[1] === "test");
|
|
|
|
if (hasTest) {
|
|
const hasUpdateSnapshots = allArgs.some(arg => arg === "-u" || arg === "--update-snapshots");
|
|
const hasTestNamePattern = allArgs.some(arg => arg === "-t" || arg === "--test-name-pattern");
|
|
|
|
if (hasUpdateSnapshots && hasTestNamePattern) {
|
|
denyWithReason("error: Cannot use -u/--update-snapshots with -t/--test-name-pattern");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if timeout option is set for "bun bd" command
|
|
if (timeout !== undefined && (argv0 === "bun" || argv0.includes("bun-debug"))) {
|
|
const positionalArgs = tokens.slice(1).filter(arg => !arg.startsWith("-"));
|
|
if (positionalArgs.length === 1 && positionalArgs[0] === "bd") {
|
|
denyWithReason("error: Run `bun bd` without a timeout");
|
|
}
|
|
}
|
|
|
|
// Check if running "bun test <file>" without USE_SYSTEM_BUN=1
|
|
if ((argv0 === "bun" || argv0.includes("bun-debug")) && useSystemBun !== "1") {
|
|
const allArgs = tokens.slice(1);
|
|
const positionalArgs = allArgs.filter(arg => !arg.startsWith("-"));
|
|
|
|
// Check if it's "test" (not "bd test")
|
|
if (positionalArgs.length >= 1 && positionalArgs[0] === "test" && positionalArgs[0] !== "bd") {
|
|
denyWithReason(
|
|
"error: In development, use `bun bd test <file>` to test your changes. If you meant to use a release version, set USE_SYSTEM_BUN=1",
|
|
);
|
|
}
|
|
}
|
|
|
|
// Check if running "bun bd test" from bun repo root or test folder without a file path
|
|
if (argv0 === "bun" || argv0.includes("bun-debug")) {
|
|
const allArgs = tokens.slice(1);
|
|
const positionalArgs = allArgs.filter(arg => !arg.startsWith("-"));
|
|
|
|
// Check if it's "bd test"
|
|
if (positionalArgs.length >= 2 && positionalArgs[0] === "bd" && positionalArgs[1] === "test") {
|
|
// Check if cwd is the bun repo root or test folder
|
|
const isBunRepoRoot = cwd === "/workspace/bun" || cwd.endsWith("/bun");
|
|
const isTestFolder = cwd.endsWith("/bun/test");
|
|
|
|
if (isBunRepoRoot || isTestFolder) {
|
|
// Check if there's a file path argument (looks like a path: contains / or has test extension)
|
|
const hasFilePath = positionalArgs
|
|
.slice(2)
|
|
.some(
|
|
arg =>
|
|
arg.includes("/") ||
|
|
arg.endsWith(".test.ts") ||
|
|
arg.endsWith(".test.js") ||
|
|
arg.endsWith(".test.tsx") ||
|
|
arg.endsWith(".test.jsx"),
|
|
);
|
|
|
|
if (!hasFilePath) {
|
|
denyWithReason(
|
|
"error: `bun bd test` from repo root or test folder will run all tests. Use `bun bd test <path>` with a specific test file.",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Allow the command to proceed
|
|
process.exit(0);
|