diff --git a/.claude/hooks/hooks.ts b/.claude/hooks/hooks.ts index 6d69089..72e539d 100644 --- a/.claude/hooks/hooks.ts +++ b/.claude/hooks/hooks.ts @@ -1,13 +1,13 @@ -import { - defineHooks, - defineHook, - logPreToolUseEvents, - logPostToolUseEvents, - logStopEvents, -} from "../.."; +import { defineHooks } from '../..'; +import { logPreToolUseEvents, logPostToolUseEvents } from '../../src/hooks/logToolUseEvents'; +import { blockEnvFiles } from '../../src/hooks/blockEnvFiles'; +import { logStopEvents } from '../../src/hooks/logStopEvents'; -export default defineHooks({ - PreToolUse: [logPreToolUseEvents({ maxEventsStored: 100 })], - PostToolUse: [logPostToolUseEvents({ maxEventsStored: 100 })], - Stop: [logStopEvents({ maxEventsStored: 100 })], -}); +defineHooks({ + PreToolUse: [ + logPreToolUseEvents(), + blockEnvFiles + ], + PostToolUse: [logPostToolUseEvents()], + Stop: [logStopEvents()] +}); \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json index 3b9b12a..b6ff3e7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,7 +6,16 @@ "hooks": [ { "type": "command", - "command": "test -f \"./.hooks/hooks.js\" && node \"./.hooks/hooks.js\" __run_hook PreToolUse \".*\" \"0\" || (>&2 echo \"Error: Hook script not found at ./.hooks/hooks.js\" && >&2 echo \"Please run: npx define-claude-code-hooks\" && exit 1) # __managed_by_define_claude_code_hooks__" + "command": "test -f \"./.claude/hooks/hooks.ts\" && npx ts-node \"./.claude/hooks/hooks.ts\" __run_hook PreToolUse \".*\" \"0\" || (>&2 echo \"Error: Hook script not found at ./.claude/hooks/hooks.ts\" && >&2 echo \"Please run: npx define-claude-code-hooks\" && exit 1) # __managed_by_define_claude_code_hooks__" + } + ] + }, + { + "matcher": "Read|Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "test -f \"./.claude/hooks/hooks.ts\" && npx ts-node \"./.claude/hooks/hooks.ts\" __run_hook PreToolUse \"Read|Write|Edit|MultiEdit\" \"1\" || (>&2 echo \"Error: Hook script not found at ./.claude/hooks/hooks.ts\" && >&2 echo \"Please run: npx define-claude-code-hooks\" && exit 1) # __managed_by_define_claude_code_hooks__" } ] } @@ -17,7 +26,7 @@ "hooks": [ { "type": "command", - "command": "test -f \"./.hooks/hooks.js\" && node \"./.hooks/hooks.js\" __run_hook PostToolUse \".*\" \"0\" || (>&2 echo \"Error: Hook script not found at ./.hooks/hooks.js\" && >&2 echo \"Please run: npx define-claude-code-hooks\" && exit 1) # __managed_by_define_claude_code_hooks__" + "command": "test -f \"./.claude/hooks/hooks.ts\" && npx ts-node \"./.claude/hooks/hooks.ts\" __run_hook PostToolUse \".*\" \"0\" || (>&2 echo \"Error: Hook script not found at ./.claude/hooks/hooks.ts\" && >&2 echo \"Please run: npx define-claude-code-hooks\" && exit 1) # __managed_by_define_claude_code_hooks__" } ] } @@ -27,7 +36,7 @@ "hooks": [ { "type": "command", - "command": "test -f \"./.hooks/hooks.js\" && node \"./.hooks/hooks.js\" __run_hook Stop || (>&2 echo \"Error: Hook script not found at ./.hooks/hooks.js\" && >&2 echo \"Please run: npx define-claude-code-hooks\" && exit 1) # __managed_by_define_claude_code_hooks__" + "command": "test -f \"./.claude/hooks/hooks.ts\" && npx ts-node \"./.claude/hooks/hooks.ts\" __run_hook Stop || (>&2 echo \"Error: Hook script not found at ./.claude/hooks/hooks.ts\" && >&2 echo \"Please run: npx define-claude-code-hooks\" && exit 1) # __managed_by_define_claude_code_hooks__" } ] } diff --git a/CLAUDE.md b/CLAUDE.md index 9b035d6..c336142 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,19 +4,28 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a TypeScript library that provides type-safe hook definitions for Claude Code with automatic settings management. It allows users to define hooks that run at various points in Claude Code's lifecycle without manually editing settings.json files. Hook files are automatically compiled from TypeScript to JavaScript for execution. +This is a TypeScript library that provides type-safe hook definitions for Claude Code with automatic settings management. It allows users to define hooks that run at various points in Claude Code's lifecycle without manually editing settings.json files. Hook files are executed directly using ts-node without compilation. -## Build Commands +## Development Commands ```bash +# Install dependencies +bun install + # Build the TypeScript project bun run build # Watch mode for development bun run dev -# Install dependencies -bun install +# Run tests +bun run test:run + +# Run tests with coverage +bun run test:coverage + +# Type checking / linting +bun run typecheck ``` ## Architecture @@ -24,41 +33,43 @@ bun install ### Key Concepts 1. **Self-Executing Hooks**: Hook files act as both the hook definition and the runner. When `defineHooks` is called, it checks if it's being run as a CLI and handles two modes: - - `__generate_settings`: Outputs JSON information about defined hooks - `__run_hook`: Executes the appropriate hook handler -2. **No Separate Runner**: Unlike traditional approaches, there's no separate runner.js. The compiled hooks file itself is executed directly by the generated commands in settings.json. +2. **No Separate Runner**: Unlike traditional approaches, there's no separate runner.js. The hooks file itself is executed directly by the generated commands in settings.json using ts-node. -3. **Automatic Compilation**: The CLI compiles TypeScript hook files to JavaScript in the `.hooks` directory: - - TypeScript hooks are compiled on-the-fly when running the CLI - - Compiled JavaScript files are placed in `.hooks/` (gitignored) - - No need for ts-node at runtime - hooks run as pure JavaScript +3. **Direct TypeScript Execution**: The CLI generates commands that use ts-node to execute TypeScript hooks directly: + - No compilation step needed + - TypeScript hooks are executed directly using ts-node + - No intermediate JavaScript files are generated 4. **Smart Settings Generation**: The CLI only creates settings entries for hooks that are actually defined: - For PreToolUse/PostToolUse: One entry per matcher - For other hooks (Stop, Notification, SubagentStop): One entry only if handlers exist + - Commands use `npx ts-node` to execute the TypeScript hooks directly 5. **Multiple Hook Files**: The system supports two different hook files, all located in `.claude/hooks/`: - - `hooks.ts` - Project hooks (compiles to `.hooks/hooks.js`, updates `.claude/settings.json`) - - `hooks.local.ts` - Local hooks (compiles to `.hooks/hooks.local.js`, updates `.claude/settings.local.json`) - - The CLI automatically detects which files exist, compiles them, and updates the corresponding settings files. + - `hooks.ts` - Project hooks (updates `.claude/settings.json`) + - `hooks.local.ts` - Local hooks (updates `.claude/settings.local.json`) ### Core Components - **src/index.ts**: Exports `defineHooks` and `defineHook` functions. Contains the self-execution logic that makes hooks files act as their own runners. -- **src/cli.ts**: The CLI that compiles TypeScript hooks to JavaScript and updates settings.json files. It automatically detects which hook files exist (hooks.ts, hooks.local.ts), compiles them to `.hooks/`, and updates the corresponding settings files. +- **src/cli.ts**: The CLI that updates settings.json files to execute TypeScript hooks using ts-node. Includes an interactive `--init` command for project setup. - **src/types.ts**: TypeScript type definitions for all hook types, inputs, and outputs. Key distinction between tool hooks (PreToolUse/PostToolUse) that have matchers and non-tool hooks. -- **src/hooks/**: Predefined hook utilities for common logging scenarios (logToolUseEvents, logStopEvents, logNotificationEvents) +- **src/hooks/**: Predefined hook utilities: + - `logToolUseEvents.ts`: Logs PreToolUse and PostToolUse events to JSON files + - `logStopEvents.ts`: Logs Stop and SubagentStop events + - `logNotificationEvents.ts`: Logs notification events + - `blockEnvFiles.ts`: Security hook that blocks access to .env files + - `announceHooks.ts`: Text-to-speech announcements for various events ### Hook Definition Structure Tool hooks (PreToolUse/PostToolUse): - ```typescript { matcher: string, // Regex pattern for tool names @@ -67,7 +78,6 @@ Tool hooks (PreToolUse/PostToolUse): ``` Non-tool hooks (Stop, Notification, SubagentStop): - ```typescript async (input) => { /* ... */ @@ -80,11 +90,32 @@ The CLI removes all hooks marked with `__managed_by_define_claude_code_hooks__` ```json { - "command": "node \"./.hooks/hooks.js\" __run_hook PreToolUse \"Bash\" \"0\" # __managed_by_define_claude_code_hooks__" + "command": "npx ts-node \"./.claude/hooks/hooks.ts\" __run_hook PreToolUse \"Bash\" \"0\" # __managed_by_define_claude_code_hooks__" } ``` -Note that commands reference the compiled JavaScript files in `.hooks/`, not the original TypeScript files. +Commands use `npx ts-node` to execute the TypeScript files directly. + +## Testing + +The project uses Vitest for testing with comprehensive coverage: + +```bash +# Run a single test file +bun run test src/hooks/__tests__/logToolUseEvents.test.ts + +# Run tests in watch mode +bun run test + +# Run tests with coverage report +bun run test:coverage +``` + +Test structure: +- Each hook has corresponding tests in `src/hooks/__tests__/` +- Test utilities in `src/__tests__/` (fixtures, mockFs, testUtils) +- Global test setup in `src/__tests__/setup.ts` +- Coverage configuration in `vitest.config.mjs` ## Development Notes @@ -93,17 +124,10 @@ Note that commands reference the compiled JavaScript files in `.hooks/`, not the - Hook source files are all located in `.claude/hooks/`: - `hooks.ts` (project-wide hooks) - `hooks.local.ts` (local-only hooks, not committed to git) -- Compiled JavaScript files are generated in `.hooks/`: - - `.hooks/hooks.js` - - `.hooks/hooks.local.js` -- The `.hooks/` directory is gitignored - Compatible with npm, yarn, pnpm, and bun package managers -- The CLI no longer requires flags - it automatically detects which hook files exist, compiles them, and updates the appropriate settings files -- No runtime dependency on ts-node - hooks execute as pure JavaScript - -### Testing - -When testing the package, create test repositories in the `tmp/` folder in the project root. This folder is already in `.gitignore` so test projects won't be committed. +- The CLI automatically detects which hook files exist and updates the appropriate settings files +- Requires ts-node as a dependency for executing TypeScript hooks +- When testing the package, create test repositories in the `tmp/` folder (already in `.gitignore`) ## Release Process @@ -153,3 +177,34 @@ For testing new features before a stable release: The package is published under the `@timoaus` scope as `@timoaus/define-claude-code-hooks`. Note: The repository must have the `NPM_TOKEN` secret configured for publishing. + +## CLI Features + +### Interactive Initialization + +The CLI provides an `--init` command for easy project setup: + +```bash +define-claude-code-hooks --init +``` + +This command: +- Uses interactive prompts to select predefined hooks +- Automatically installs the package if needed (including ts-node) +- Detects the package manager (npm/yarn/pnpm/bun) +- Adds a `claude:hooks` script to package.json +- Creates initial hook files with selected hooks + +### Automatic Hook Detection + +The CLI automatically: +- Scans for hook files in `.claude/hooks/` +- Generates commands that use ts-node to execute the TypeScript files +- Updates corresponding settings files +- Preserves user-defined hooks while managing its own + +# important-instruction-reminders +Do what has been asked; nothing more, nothing less. +NEVER create files unless they're absolutely necessary for achieving your goal. +ALWAYS prefer editing an existing file to creating a new one. +NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. \ No newline at end of file diff --git a/bun.lock b/bun.lock index a73e91c..b7bf33e 100644 --- a/bun.lock +++ b/bun.lock @@ -6,9 +6,11 @@ "dependencies": { "@types/inquirer": "^9.0.8", "inquirer": "^12.7.0", - "typescript": "^5.0.0", + "ts-node": "^10.9.2", + "typescript": "^5", }, "devDependencies": { + "@timoaus/define-claude-code-hooks": "^1.3.0", "@types/bun": "^1.2.18", "@types/node": "^24.0.10", "@vitest/coverage-v8": "^3.2.4", @@ -33,6 +35,8 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.6", "", { "os": "aix", "cpu": "ppc64" }, "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.6", "", { "os": "android", "cpu": "arm" }, "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg=="], @@ -183,6 +187,16 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.2", "", { "os": "win32", "cpu": "x64" }, "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA=="], + "@timoaus/define-claude-code-hooks": ["@timoaus/define-claude-code-hooks@1.3.0", "", { "dependencies": { "typescript": "^5.0.0" }, "bin": { "define-claude-code-hooks": "dist/cli.js" } }, "sha512-YVfzTzDgshEDzEbMNBgcM92NNFJNCln2GXMlCcllaSR6aYMSda4kvjuvI3ZHk5Eqoy67PJoc7AXyfueEXjYLfQ=="], + + "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], + + "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], + + "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="], + + "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], @@ -217,12 +231,18 @@ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw=="], @@ -247,6 +267,8 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], @@ -255,6 +277,8 @@ "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -313,6 +337,8 @@ "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -397,6 +423,8 @@ "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], @@ -405,6 +433,8 @@ "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], + "vite": ["vite@7.0.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -421,8 +451,12 @@ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="], + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], diff --git a/package.json b/package.json index 22eb1aa..2c328b1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "dependencies": { "@types/inquirer": "^9.0.8", "inquirer": "^12.7.0", + "ts-node": "^10.9.2", "typescript": "^5" }, "devDependencies": { diff --git a/src/cli.ts b/src/cli.ts index 45d3ebc..8a7da24 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,6 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { HookSettings, HookDefinition, HookType } from './types'; -import * as ts from 'typescript'; import inquirer from 'inquirer'; import { execSync } from 'child_process'; @@ -13,11 +12,6 @@ const HOOK_FILES = { local: '.claude/hooks/hooks.local.ts' } as const; -const COMPILED_HOOK_FILES = { - project: '.hooks/hooks.js', - local: '.hooks/hooks.local.js' -} as const; - const MANAGED_BY_MARKER = '__managed_by_define_claude_code_hooks__'; function getManagedMarker(location: 'project' | 'local'): string { @@ -93,119 +87,11 @@ Options: This command will automatically detect hook files in .claude/hooks/: - hooks.ts → Updates .claude/settings.json - hooks.local.ts → Updates .claude/settings.local.json + +Note: Requires ts-node to be installed in your project. `); } -async function compileHookFile(tsPath: string, jsPath: string, location: 'project' | 'local'): Promise { - try { - // Read the TypeScript file - let sourceCode = fs.readFileSync(tsPath, 'utf-8'); - - // For development: replace imports from the package name to relative paths - const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')); - const packageName = packageJson.name; - - // Check if we're in the package's own repository - const isOwnRepo = fs.existsSync(path.join(process.cwd(), 'src', 'index.ts')) && - fs.existsSync(path.join(process.cwd(), 'package.json')); - - if (isOwnRepo) { - // Replace package imports with relative imports to dist - sourceCode = sourceCode.replace( - new RegExp(`from\s+['"]${packageName}['"]`, 'g'), - `from '${path.relative(path.dirname(jsPath), path.join(process.cwd(), 'dist', 'index'))}'` - ); - - // Also handle relative imports like "../.." that point to the package root - sourceCode = sourceCode.replace( - /from\s+['"]\.\.\/(\.\.)?['"](?!\w)/g, - `from '${path.relative(path.dirname(jsPath), path.join(process.cwd(), 'dist', 'index'))}'` - ); - } - - // Detect module type from project's package.json - let isESModule = false; - try { - const projectPackageJsonPath = path.join(process.cwd(), 'package.json'); - if (fs.existsSync(projectPackageJsonPath)) { - const projectPackageJson = JSON.parse(fs.readFileSync(projectPackageJsonPath, 'utf-8')); - isESModule = projectPackageJson.type === 'module'; - } - } catch (error) { - // If we can't read the package.json, default to CommonJS - isESModule = false; - } - - // Compile TypeScript to JavaScript - const result = ts.transpileModule(sourceCode, { - compilerOptions: { - module: isESModule ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ES2020, - esModuleInterop: true, - allowSyntheticDefaultImports: true, - moduleResolution: ts.ModuleResolutionKind.NodeJs, - resolveJsonModule: true, - skipLibCheck: true - } - }); - - // If we're in the package's own repository, fix the import/require paths in the compiled output - let outputText = result.outputText; - if (isOwnRepo) { - const relativePath = path.relative(path.dirname(jsPath), path.join(process.cwd(), 'dist', 'index')).replace(/\\/g, '/'); - - if (isESModule) { - // For ES modules, replace import statements - const packageNameEscaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - - // Replace import statements - outputText = outputText.replace( - new RegExp(`from\\s+["']${packageNameEscaped}["']`, 'g'), - `from '${relativePath}'` - ); - - // Handle relative imports like from "../.." that point to the package root - outputText = outputText.replace( - /from\s+["']\.\.\/(\.\.)?["']/g, - `from '${relativePath}'` - ); - } else { - // For CommonJS, replace require statements - const packageNameEscaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const regex = new RegExp(`require\\(["']${packageNameEscaped}["']\\)`, 'g'); - outputText = outputText.replace( - regex, - `require('${relativePath}')` - ); - - // Also try without escaping the parentheses - outputText = outputText.replace( - new RegExp(`require\(["']${packageName}["']\)`, 'g'), - `require('${relativePath}')` - ); - - // Handle relative requires like require("../..") that point to the package root - outputText = outputText.replace( - /require\(["']\.\.\/(\.\.)?["']\)/g, - `require('${relativePath}')` - ); - } - } - - // Ensure .hooks directory exists - const jsDir = path.dirname(jsPath); - if (!fs.existsSync(jsDir)) { - fs.mkdirSync(jsDir, { recursive: true }); - } - - // Write the compiled JavaScript - fs.writeFileSync(jsPath, outputText); - return true; - } catch (error) { - console.error(`Failed to compile ${tsPath}:`, error); - return false; - } -} async function updateHooks(options: CliOptions) { let updatedAny = false; @@ -213,7 +99,6 @@ async function updateHooks(options: CliOptions) { // Check for each type of hook file for (const [type, fileName] of Object.entries(HOOK_FILES)) { const hookFilePath = path.resolve(process.cwd(), fileName); - const compiledHookPath = path.resolve(process.cwd(), COMPILED_HOOK_FILES[type as keyof typeof COMPILED_HOOK_FILES]); // Determine settings file path let settingsPath: string; @@ -231,28 +116,12 @@ async function updateHooks(options: CliOptions) { if (fs.existsSync(hookFilePath)) { console.log(`Found ${fileName}`); - // Compile the TypeScript file - console.log(`Compiling ${fileName} to ${COMPILED_HOOK_FILES[type as keyof typeof COMPILED_HOOK_FILES]}...`); - const compiled = await compileHookFile(hookFilePath, compiledHookPath, type as 'project' | 'local'); - - if (!compiled) { - console.error(`Failed to compile ${fileName}`); - continue; - } - - // Get hook information by running the compiled hooks file + // Get hook information by running the TypeScript hooks file with ts-node let hookInfo: any; - // Check if the compiled file exists before trying to run it - if (!fs.existsSync(compiledHookPath)) { - console.error(`Error: Compiled hook file not found at ${compiledHookPath}`); - console.error('The compilation step may have failed. Please check for TypeScript errors.'); - continue; - } - try { const output = execSync( - `node "${compiledHookPath}" __generate_settings`, + `npx ts-node "${hookFilePath}" __generate_settings`, { encoding: 'utf-8' } ); @@ -264,7 +133,7 @@ async function updateHooks(options: CliOptions) { await updateSettingsFile( settingsPath, - compiledHookPath, + hookFilePath, hookInfo, type as 'project' | 'local' ); @@ -311,12 +180,12 @@ async function removeHooks(options: CliOptions) { async function updateSettingsFile( settingsPath: string, - compiledHookPath: string, + hookFilePath: string, hookInfo: any, location: 'project' | 'local' ) { // Use relative path for project/local - const commandPath = `./${COMPILED_HOOK_FILES[location]}`; + const commandPath = `./${HOOK_FILES[location]}`; // Ensure directory exists const dir = path.dirname(settingsPath); if (!fs.existsSync(dir)) { @@ -369,7 +238,7 @@ async function updateSettingsFile( matcher: entry.matcher, hooks: [{ type: 'command', - command: `test -f "${commandPath}" && node "${commandPath}" __run_hook ${hookType} "${entry.matcher}" "${entry.index}" || (>&2 echo "Error: Hook script not found at ${commandPath}" && >&2 echo "Please run: npx define-claude-code-hooks" && exit 1) # ${marker}` + command: `test -f "${commandPath}" && npx ts-node "${commandPath}" __run_hook ${hookType} "${entry.matcher}" "${entry.index}" || (>&2 echo "Error: Hook script not found at ${commandPath}" && >&2 echo "Please run: npx define-claude-code-hooks" && exit 1) # ${marker}` }] }); } else { @@ -377,7 +246,7 @@ async function updateSettingsFile( settings.hooks[typedHookType]!.push({ hooks: [{ type: 'command', - command: `test -f "${commandPath}" && node "${commandPath}" __run_hook ${hookType} || (>&2 echo "Error: Hook script not found at ${commandPath}" && >&2 echo "Please run: npx define-claude-code-hooks" && exit 1) # ${marker}` + command: `test -f "${commandPath}" && npx ts-node "${commandPath}" __run_hook ${hookType} || (>&2 echo "Error: Hook script not found at ${commandPath}" && >&2 echo "Please run: npx define-claude-code-hooks" && exit 1) # ${marker}` }] }); }