From 0268422c81e1f720842af95e4a69044b6e64e902 Mon Sep 17 00:00:00 2001 From: Mats Bosson Date: Sun, 29 Mar 2026 05:18:07 +0700 Subject: [PATCH] NagLint hook setup --- .claude/hooks/naglint.sh | 77 +++++++++++++++++++ .claude/settings.json | 15 ++++ .../src/utilities/dismiss/create-dismiss.ts | 10 +-- 3 files changed, 97 insertions(+), 5 deletions(-) create mode 100755 .claude/hooks/naglint.sh create mode 100644 .claude/settings.json diff --git a/.claude/hooks/naglint.sh b/.claude/hooks/naglint.sh new file mode 100755 index 0000000..3f316fa --- /dev/null +++ b/.claude/hooks/naglint.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# NagLint PostToolUse hook — runs after Write/Edit on .ts/.tsx/.js/.jsx files +# Reads tool_use JSON from stdin, extracts file_path, runs nag --json + +NAG=/Users/matsbosson/Documents/StayThree/NagLint/target/release/nag + +INPUT=$(cat -) +FILE=$(echo "$INPUT" | python3 -c " +import sys, json +d = json.load(sys.stdin) +print(d.get('tool_input', {}).get('file_path', '')) +" 2>/dev/null) + +if [ -z "$FILE" ]; then + echo "{}" + exit 0 +fi + +# Only lint source files — skip config, json, yaml, md, etc. +case "$FILE" in + *.ts|*.tsx|*.js|*.jsx) ;; + *) echo "{}"; exit 0 ;; +esac + +if [ ! -f "$FILE" ]; then + echo "{}" + exit 0 +fi + +RAW=$("$NAG" "$FILE" --json 2>/dev/null) + +if [ "$RAW" = "{}" ] || [ -z "$RAW" ]; then + echo "{}" + exit 0 +fi + +# For .tsx/.jsx files: filter out AI-08 (logic density) — JSX is declarative +# and the 30% threshold doesn't apply to component files. +case "$FILE" in + *.tsx|*.jsx) + FILTERED=$(echo "$RAW" | python3 -c " +import sys, json + +raw = sys.stdin.read().strip() +try: + d = json.loads(raw) +except Exception: + print(raw) + sys.exit(0) + +# If no decision:block, pass through as-is +if d.get('decision') != 'block': + print(raw) + sys.exit(0) + +reason = d.get('reason', '') +# Filter AI-08 lines from reason +filtered_lines = [l for l in reason.splitlines() if '[AI-08]' not in l] +filtered_reason = '\n'.join(filtered_lines).strip() + +if not filtered_reason: + # All violations were AI-08 — file is clean for our purposes + print('{}') +else: + d['reason'] = filtered_reason + # Also update additionalContext + ctx = d.get('hookSpecificOutput', {}).get('additionalContext', '') + ctx_lines = [l for l in ctx.splitlines() if '[AI-08]' not in l] + d['hookSpecificOutput']['additionalContext'] = '\n'.join(ctx_lines) + print(json.dumps(d)) +" 2>/dev/null) + echo "${FILTERED:-$RAW}" + ;; + *) + echo "$RAW" + ;; +esac diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..df12726 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "/Users/matsbosson/Documents/StayThree/PettyUI/.claude/hooks/naglint.sh" + } + ] + } + ] + } +} diff --git a/packages/core/src/utilities/dismiss/create-dismiss.ts b/packages/core/src/utilities/dismiss/create-dismiss.ts index 2f51350..1f84da8 100644 --- a/packages/core/src/utilities/dismiss/create-dismiss.ts +++ b/packages/core/src/utilities/dismiss/create-dismiss.ts @@ -14,27 +14,27 @@ export interface Dismiss { detach: () => void; } +// Global stack of active dismiss handlers (topmost is last) +const layerStack: Dismiss[] = []; + /** * Handles dismiss interactions: Escape key and pointer-outside. * Uses a global layer stack so nested overlays only dismiss the topmost layer. */ - -// Global stack of active dismiss handlers (topmost is last) -const layerStack: Dismiss[] = []; - export function createDismiss(options: CreateDismissOptions): Dismiss { const dismissOnEscape = options.dismissOnEscape ?? true; const dismissOnPointerOutside = options.dismissOnPointerOutside ?? true; + /** Dismisses on Escape if this is the topmost layer and escape is enabled. */ const handleKeyDown = (e: KeyboardEvent) => { if (!dismissOnEscape) return; if (e.key !== "Escape") return; - // Only dismiss the topmost layer if (layerStack[layerStack.length - 1] !== dismiss) return; e.preventDefault(); options.onDismiss(); }; + /** Dismisses on pointer-outside if this is the topmost layer and pointer-outside is enabled. */ const handlePointerDown = (e: PointerEvent) => { if (!dismissOnPointerOutside) return; if (layerStack[layerStack.length - 1] !== dismiss) return;