NagLint hook setup
This commit is contained in:
parent
523c908b20
commit
0268422c81
77
.claude/hooks/naglint.sh
Executable file
77
.claude/hooks/naglint.sh
Executable file
@ -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
|
||||||
15
.claude/settings.json
Normal file
15
.claude/settings.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Write|Edit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "/Users/matsbosson/Documents/StayThree/PettyUI/.claude/hooks/naglint.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,27 +14,27 @@ export interface Dismiss {
|
|||||||
detach: () => void;
|
detach: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global stack of active dismiss handlers (topmost is last)
|
||||||
|
const layerStack: Dismiss[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles dismiss interactions: Escape key and pointer-outside.
|
* Handles dismiss interactions: Escape key and pointer-outside.
|
||||||
* Uses a global layer stack so nested overlays only dismiss the topmost layer.
|
* 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 {
|
export function createDismiss(options: CreateDismissOptions): Dismiss {
|
||||||
const dismissOnEscape = options.dismissOnEscape ?? true;
|
const dismissOnEscape = options.dismissOnEscape ?? true;
|
||||||
const dismissOnPointerOutside = options.dismissOnPointerOutside ?? true;
|
const dismissOnPointerOutside = options.dismissOnPointerOutside ?? true;
|
||||||
|
|
||||||
|
/** Dismisses on Escape if this is the topmost layer and escape is enabled. */
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (!dismissOnEscape) return;
|
if (!dismissOnEscape) return;
|
||||||
if (e.key !== "Escape") return;
|
if (e.key !== "Escape") return;
|
||||||
// Only dismiss the topmost layer
|
|
||||||
if (layerStack[layerStack.length - 1] !== dismiss) return;
|
if (layerStack[layerStack.length - 1] !== dismiss) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
options.onDismiss();
|
options.onDismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Dismisses on pointer-outside if this is the topmost layer and pointer-outside is enabled. */
|
||||||
const handlePointerDown = (e: PointerEvent) => {
|
const handlePointerDown = (e: PointerEvent) => {
|
||||||
if (!dismissOnPointerOutside) return;
|
if (!dismissOnPointerOutside) return;
|
||||||
if (layerStack[layerStack.length - 1] !== dismiss) return;
|
if (layerStack[layerStack.length - 1] !== dismiss) return;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user