Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:50:12 +08:00
commit d16c5de665
29 changed files with 4788 additions and 0 deletions

324
hooks/type-checker.ts Executable file
View File

@@ -0,0 +1,324 @@
#!/usr/bin/env bun
/**
* Type Checker Hook
*
* Pre-validates TypeScript types before Edit/Write operations.
* Uses @jbabin91/tsc-files programmatic API for incremental type checking with tsgo support.
* Informs about type errors but allows file modifications (informative only).
*
* Smart behavior:
* - Skips non-TypeScript files (no performance impact)
* - Skips projects without tsconfig.json
* - Dynamically imports type checker only when needed
*
* Performance: ~100-200ms with tsgo (10x faster than tsc)
* Performance target: <2s (ADR-0010) - typically well under this
* Hook behavior: Informative (does not block execution)
*
* @see {@link https://github.com/jbabin91/super-claude} for documentation
*/
import { existsSync } from 'node:fs';
import path from 'node:path';
import type { CheckResult } from '@jbabin91/tsc-files';
import {
checkHookEnabled,
checkPerformance,
ensureBunInstalled,
formatError,
parseStdin,
} from './utils/index.js';
/**
* Check if file is TypeScript
*
* @param filePath File path to check
* @returns true if TypeScript file
*/
function isTypeScriptFile(filePath: string): boolean {
return /\.(ts|tsx)$/.test(filePath);
}
/**
* Check if tsconfig.json exists
*
* @param cwd Current working directory
* @returns true if tsconfig.json found
*/
function hasTsConfig(cwd: string): boolean {
return (
existsSync(path.join(cwd, 'tsconfig.json')) ||
existsSync(path.join(cwd, 'tsconfig.base.json'))
);
}
/**
* Run TypeScript type checking on file using programmatic API
*
* @param cwd Current working directory
* @param filePath File path to check
* @returns CheckResult with structured error data
*/
async function checkTypes(cwd: string, filePath: string): Promise<CheckResult> {
try {
// Dynamically import checkFiles only when needed
// This prevents loading TypeScript dependencies for non-TS files
const module = await import('@jbabin91/tsc-files');
// Defensive check: ensure checkFiles exists after import
if (!module.checkFiles || typeof module.checkFiles !== 'function') {
throw new Error(
'Type checker module loaded but checkFiles function not found. ' +
'This may indicate a version mismatch. ' +
'Try: bun install @jbabin91/tsc-files@latest',
);
}
const { checkFiles } = module;
// Use programmatic API for structured error data
// Automatically uses tsgo if available (10x faster)
const result = await checkFiles([filePath], {
cwd,
skipLibCheck: true,
verbose: false,
throwOnError: false,
});
return result;
} catch (error: unknown) {
// Handle different error scenarios with helpful messages
const errorMessage = error instanceof Error ? error.message : String(error);
// Check if this is a module not found error
const isModuleNotFound =
errorMessage.includes('Cannot find package') ||
errorMessage.includes('Cannot find module') ||
errorMessage.includes('@jbabin91/tsc-files');
let helpfulMessage = errorMessage;
if (isModuleNotFound) {
helpfulMessage =
'Type checker dependency not installed.\n\n' +
'To enable type checking, install the required package:\n' +
' bun install @jbabin91/tsc-files\n\n' +
'Or disable this hook in .claude/super-claude-config.json:\n' +
' "workflow": { "hooks": { "typeChecker": { "enabled": false } } }\n\n' +
`Original error: ${errorMessage}`;
}
// Return a failed result with helpful error message
return {
success: false,
errorCount: 1,
warningCount: 0,
errors: [
{
file: filePath,
line: 0,
column: 0,
message: helpfulMessage,
code: isModuleNotFound ? 'HOOK_ERROR' : 'TS0000',
severity: 'error',
},
],
warnings: [],
checkedFiles: [filePath],
duration: 0,
};
}
}
/**
* Format type errors for display with categorization
*
* @param result CheckResult from tsc-files API
* @param targetFile The file being edited
* @returns Formatted error message
*/
function formatTypeErrors(result: CheckResult, targetFile: string): string {
// Check if this is a hook error (not a type error)
const hasHookError = result.errors.some((e) => e.code === 'HOOK_ERROR');
if (hasHookError) {
// Format hook configuration errors differently
const hookError = result.errors.find((e) => e.code === 'HOOK_ERROR');
return [
'',
'═'.repeat(70),
'⚠️ TYPE CHECKER HOOK ERROR',
'═'.repeat(70),
'',
hookError?.message ?? 'Unknown hook error',
'',
'═'.repeat(70),
'',
].join('\n');
}
// Categorize errors by file
const targetFileErrors = result.errors.filter((e) => e.file === targetFile);
const dependencyErrors = result.errors.filter((e) => e.file !== targetFile);
// Header
const sections: string[] = [
'',
'═'.repeat(70),
'⚠️ TYPE ERRORS DETECTED - ACTION REQUIRED',
'═'.repeat(70),
'',
];
// Target file errors (critical)
if (targetFileErrors.length > 0) {
sections.push(
'🎯 ERRORS IN THIS FILE:',
` File: ${targetFile}`,
' Action: Fix these before proceeding to next task',
'',
);
for (const err of targetFileErrors.slice(0, 10)) {
sections.push(
` ${err.file}:${err.line}:${err.column}`,
` ${err.code}: ${err.message}`,
'',
);
}
if (targetFileErrors.length > 10) {
sections.push(
` ... and ${targetFileErrors.length - 10} more errors in this file`,
'',
);
}
}
// Dependency errors (informational)
if (dependencyErrors.length > 0) {
sections.push(
'─'.repeat(70),
' ERRORS IN DEPENDENCIES:',
' These errors are in imported files',
' Fix them separately or add to your todo list',
'',
);
// Group by file
const byFile = new Map<string, CheckResult['errors'][number][]>();
for (const err of dependencyErrors) {
if (!byFile.has(err.file)) {
byFile.set(err.file, []);
}
byFile.get(err.file)!.push(err);
}
let fileCount = 0;
for (const [file, errors] of byFile.entries()) {
if (fileCount >= 5) break;
sections.push(` 📄 ${file} (${errors.length} errors)`);
fileCount++;
}
if (byFile.size > 5) {
sections.push(` ... and ${byFile.size - 5} more files`);
}
sections.push('');
}
// Footer with workflow guidance
sections.push(
'─'.repeat(70),
'🤖 CLAUDE: Type errors detected.',
'',
'Recommended workflow:',
' 1. If working on a task: Add "Fix type errors" to your todo list',
' 2. Complete your current task first',
' 3. Then fix these type errors before moving to next task',
'',
'If the type error is directly related to your current edit:',
' → Fix it immediately as part of this change',
'',
'User: To disable this hook, add to .claude/settings.json:',
' { "customHooks": { "typeChecker": { "enabled": false } } }',
'═'.repeat(70),
'',
);
return sections.join('\n');
}
/**
* Main hook execution
*/
async function main(): Promise<void> {
const startTime = Date.now();
try {
// Ensure Bun is installed (fail fast with helpful message)
ensureBunInstalled();
const input = await parseStdin();
// Check if hook is enabled
checkHookEnabled(input.cwd, 'typeChecker');
// Only run for Edit and Write tools
if (input.tool_name !== 'Edit' && input.tool_name !== 'Write') {
process.exit(0); // Not a file modification tool
}
// Extract file path from tool input
const toolInput = input.tool_input!;
const filePath = toolInput?.file_path as string | undefined;
if (!filePath) {
process.exit(0); // No file path
}
// Skip if not TypeScript file
if (!isTypeScriptFile(filePath)) {
process.exit(0);
}
// Skip if no tsconfig
if (!hasTsConfig(input.cwd)) {
process.exit(0);
}
// Check types using programmatic API
const result = await checkTypes(input.cwd, filePath);
if (!result.success) {
// Type errors found - inform but allow operation
const errorMessage = formatTypeErrors(result, filePath);
// Output hookSpecificOutput to inform (not block)
const output = {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'allow', // Informative only
permissionDecisionReason: errorMessage,
},
};
console.log(JSON.stringify(output));
checkPerformance(startTime, 2000, 'type-checker');
process.exit(0); // Exit 0 after informing
}
// Types are valid - allow operation silently
checkPerformance(startTime, 2000, 'type-checker');
process.exit(0);
} catch (error) {
console.error(formatError(error, 'type-checker'));
// On hook error, don't block the operation
process.exit(0);
}
}
// Execute
await main();