Bifrost/tools/git-gate/gh-proxy.mjs
2026-06-17 07:53:38 +01:00

118 lines
2.7 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from "node:child_process";
const args = process.argv.slice(2);
const realGh = process.env.BIFROST_REAL_GH;
const realGhArgs = splitCommandArgs(process.env.BIFROST_REAL_GH_ARGS ?? "");
const mutatingCommands = new Set([
"gist create",
"gist delete",
"gist edit",
"issue close",
"issue comment",
"issue create",
"issue delete",
"issue edit",
"issue lock",
"issue reopen",
"pr close",
"pr comment",
"pr create",
"pr edit",
"pr lock",
"pr merge",
"pr ready",
"pr reopen",
"pr review",
"release create",
"release delete",
"release edit",
"repo archive",
"repo create",
"repo delete",
"repo edit",
"repo fork",
"repo rename",
"secret delete",
"secret set",
"variable delete",
"variable set",
]);
if (!realGh) {
process.stderr.write("Bifrost GitHub gate has no real GitHub CLI executable configured.\n");
process.exit(1);
}
if (
process.env.BIFROST_ENFORCE_GITHUB_GATE === "true" &&
isMutatingGitHubCliCommand(args) &&
process.env.BIFROST_GITHUB_MUTATION_AUTHORIZED !== "true"
) {
process.stderr.write(
"Bifrost blocked GitHub CLI mutation in this dispatched turn. Use tools/bifrost-bridge.mjs so GitHub publication is gated and receipted.\n",
);
process.exit(1);
}
const result = isWindowsCommandScript(realGh)
? spawnSync("cmd.exe", ["/d", "/s", "/c", realGh, ...realGhArgs, ...args], {
stdio: "inherit",
env: process.env,
windowsHide: true,
})
: spawnSync(realGh, [...realGhArgs, ...args], {
stdio: "inherit",
env: process.env,
windowsHide: true,
});
if (result.error) {
throw result.error;
}
process.exit(result.status ?? 1);
function isWindowsCommandScript(command) {
return /\.cmd$/i.test(command) || /\.bat$/i.test(command);
}
function isMutatingGitHubCliCommand(input) {
const [scope = "", action = ""] = input;
if (!scope) {
return false;
}
if (scope === "api") {
return apiMethod(input) !== "GET";
}
return mutatingCommands.has(`${scope} ${action}`);
}
function apiMethod(input) {
for (let index = 0; index < input.length; index += 1) {
const token = input[index];
if (token === "--method") {
return `${input[index + 1] ?? "GET"}`.trim().toUpperCase();
}
if (token.startsWith("--method=")) {
return token.slice("--method=".length).trim().toUpperCase();
}
}
return input.includes("--input") ? "POST" : "GET";
}
function splitCommandArgs(value) {
if (!value) {
return [];
}
const matches = value.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [];
return matches
.map((part) => part.trim())
.filter(Boolean)
.map((part) => (part.startsWith("\"") && part.endsWith("\"")) ? part.slice(1, -1) : part);
}