From 0d440bed7acd931992570117fee34844fd962766 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Tue, 5 May 2026 18:28:07 +0200 Subject: [PATCH] fix: block extension declaration deletions --- package.json | 3 +- scripts/check-protected-deletions.mjs | 40 +++++++++++++++++++++++++++ scripts/install-hooks.mjs | 31 +++++++++++++++++---- 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 scripts/check-protected-deletions.mjs diff --git a/package.json b/package.json index de38c0e99..d71ecc021 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ "typecheck": "npm run build:pi && tsc --noEmit", "typecheck:extensions": "npm run check:versioned-json && tsc --noEmit --project tsconfig.extensions.json", "check:sf-inventory": "node scripts/check-sf-extension-inventory.mjs", - "check:versioned-json": "node scripts/check-versioned-json.mjs && npm run check:sf-inventory", + "check:protected-deletions": "node scripts/check-protected-deletions.mjs", + "check:versioned-json": "node scripts/check-protected-deletions.mjs && node scripts/check-versioned-json.mjs && npm run check:sf-inventory", "format": "biome format --write .", "format:check": "biome format .", "lint": "npm run check:versioned-json && biome check .", diff --git a/scripts/check-protected-deletions.mjs b/scripts/check-protected-deletions.mjs new file mode 100644 index 000000000..339578dce --- /dev/null +++ b/scripts/check-protected-deletions.mjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; + +const PROTECTED_PATHS = [":(glob)src/resources/extensions/**/*.d.ts"]; + +function git(args) { + return execFileSync("git", args, { + stdio: ["ignore", "pipe", "pipe"], + encoding: "utf-8", + }).trim(); +} + +function listDeleted(cached) { + const args = ["diff", "--name-only", "--diff-filter=D"]; + if (cached) args.push("--cached"); + args.push("--", ...PROTECTED_PATHS); + const out = git(args); + return out ? out.split("\n").filter(Boolean) : []; +} + +const stagedOnly = process.argv.includes("--cached"); +const deleted = stagedOnly + ? listDeleted(true) + : [...new Set([...listDeleted(false), ...listDeleted(true)])]; + +if (deleted.length > 0) { + const mode = stagedOnly ? "staged " : ""; + process.stderr.write( + `check-protected-deletions: refusing ${mode}protected declaration deletions:\n`, + ); + for (const path of deleted) { + process.stderr.write(` ${path}\n`); + } + process.stderr.write( + "\nRestore these files or make an explicit reviewed deletion outside automation.\n", + ); + process.exit(1); +} + +process.stdout.write("check-protected-deletions: ok\n"); diff --git a/scripts/install-hooks.mjs b/scripts/install-hooks.mjs index f34a3c889..73156507d 100644 --- a/scripts/install-hooks.mjs +++ b/scripts/install-hooks.mjs @@ -11,6 +11,7 @@ import { import { join } from "node:path"; const MARKER = "# sf-secret-scan"; +const PROTECTED_DELETIONS_MARKER = "# sf-protected-deletions"; function git(args) { return execFileSync("git", args, { @@ -24,19 +25,35 @@ const repoRoot = git(["rev-parse", "--show-toplevel"]); const hookDir = join(gitDir, "hooks"); const hookFile = join(hookDir, "pre-commit"); const hookCommand = `node "${join(repoRoot, "scripts", "secret-scan.mjs")}"`; +const protectedDeletionsCommand = `node "${join( + repoRoot, + "scripts", + "check-protected-deletions.mjs", +)}" --cached`; mkdirSync(hookDir, { recursive: true }); if (existsSync(hookFile)) { const current = readFileSync(hookFile, "utf8"); - if (current.includes(MARKER)) { - process.stdout.write("secret-scan pre-commit hook already installed.\n"); + const additions = []; + if (!current.includes(MARKER)) { + additions.push(MARKER, hookCommand); + } + if (!current.includes(PROTECTED_DELETIONS_MARKER)) { + additions.push( + PROTECTED_DELETIONS_MARKER, + "# Pre-commit hook: block accidental deletion of hand-written extension declarations", + protectedDeletionsCommand, + ); + } + if (additions.length === 0) { + process.stdout.write("sf pre-commit hooks already installed.\n"); process.exit(0); } - const next = `${current.replace(/\s*$/, "\n")}${MARKER}\n${hookCommand}\n`; + const next = `${current.replace(/\s*$/, "\n")}${additions.join("\n")}\n`; writeFileSync(hookFile, next, "utf8"); - process.stdout.write("secret-scan appended to existing pre-commit hook.\n"); + process.stdout.write("sf pre-commit hooks appended to existing hook.\n"); process.exit(0); } @@ -46,6 +63,10 @@ const hookBody = [ "# Pre-commit hook: scan staged files for hardcoded secrets", hookCommand, "", + "# sf-protected-deletions", + "# Pre-commit hook: block accidental deletion of hand-written extension declarations", + protectedDeletionsCommand, + "", ].join("\n"); writeFileSync(hookFile, hookBody, "utf8"); @@ -55,4 +76,4 @@ try { // Best effort on Windows filesystems that do not honor chmod. } -process.stdout.write("secret-scan pre-commit hook installed.\n"); +process.stdout.write("sf pre-commit hooks installed.\n");