/** * Commit Guard Extension * * WHAT: On `git commit -m …`, validates the message against Conventional * Commits and scans it for secrets. Invalid/secret → confirm (UI) or * block (no UI). Non-`-m` commits (editor) are left alone. * * CONFIGURE: edit COMMIT_TYPES below. Secret patterns live in * extensions/lib/secrets.ts. * * DISABLE: remove/rename this file from the package `extensions/` folder. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { scanSecrets } from "./lib/secrets.ts"; import { audit } from "./lib/audit.ts"; // Conventional Commits types accepted in the header. Extend as needed. const COMMIT_TYPES = ["feat", "fix", "chore", "docs", "refactor", "test", "perf", "build", "ci", "style", "revert"]; const HEADER_RE = new RegExp(`^(${COMMIT_TYPES.join("|")})(\\(.+\\))?!?: .+`); /** Extract the -m / --message value from a `git commit` command, if present. */ function extractCommitMessage(command: string): string | null { // Matches -m "msg", -m 'msg', --message=msg, -m msg (best effort). const m = command.match(/(?:-m|--message)[=\s]+"([^"]*)"/) ?? command.match(/(?:-m|--message)[=\s]+'([^']*)'/) ?? command.match(/(?:-m|--message)[=\s]+(\S+)/); return m ? m[1] : null; } export default function commitGuardExtension(pi: ExtensionAPI) { pi.on("tool_call", async (event, ctx) => { if (event.toolName !== "bash") return undefined; const command = (event.input.command as string | undefined) ?? ""; if (!/\bgit\s+commit\b/.test(command)) return undefined; const message = extractCommitMessage(command); if (message === null) return undefined; // editor-based commit; not our concern const problems: string[] = []; if (!HEADER_RE.test(message)) problems.push("не Conventional Commits"); const secretHits = scanSecrets(message); if (secretHits.length > 0) problems.push(`секрет (${secretHits.join(", ")})`); if (problems.length === 0) return undefined; audit({ kind: "guardrail_block", guardrail: "commit-guard", problems, hasSecret: secretHits.length > 0 }); if (ctx.hasUI) { const choice = await ctx.ui.select( `⚠️ Проблема с сообщением коммита: ${problems.join("; ")}.\nФормат: type(scope): описание. Всё равно закоммитить?`, ["Нет", "Да"], ); if (choice === "Да") return undefined; } return { block: true, reason: `Commit message rejected: ${problems.join("; ")}` }; }); }