diff --git a/.github/workflows/ci.yml b/.github/workflows/lint-and-test.yml similarity index 68% rename from .github/workflows/ci.yml rename to .github/workflows/lint-and-test.yml index 32f2be6..9d6f622 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/lint-and-test.yml @@ -1,9 +1,7 @@ -name: CI +name: Lint & Test on: pull_request: - push: - branches: [main] - + workflow_call: jobs: lint-and-test: runs-on: ubuntu-latest @@ -13,12 +11,9 @@ jobs: with: node-version-file: '.nvmrc' cache: 'npm' - - run: npm ci - - # Run prettier check (fails if formatting is wrong) + - name: Install deps + run: npm ci - name: Run Prettier run: npm run lint:prettier - - # Run tests only if Prettier passed - name: Run Tests run: npm test -- --runInBand diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5fb59f8..99ad7f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,17 +1,15 @@ -name: Release +name: Release & Publish on: - workflow_run: - workflows: ['CI'] - types: [completed] + push: + branches: ['main', 'rc', 'develop'] jobs: - release: - # Only run if CI succeeded AND the source branch was main - if: > - ${{ - github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.head_branch == 'main' - }} + quality: + uses: ./.github/workflows/lint-and-test.yml + + publish: + name: Semantic Release + needs: quality runs-on: ubuntu-latest permissions: contents: write @@ -19,25 +17,20 @@ jobs: issues: write id-token: write steps: - - name: Check out the exact commit from the CI run + - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - ref: ${{ github.event.workflow_run.head_sha }} - - uses: actions/setup-node@v4 + - name: Setup Node + uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'npm' registry-url: 'https://registry.npmjs.org/' - - run: npm ci - - - name: Run Prettier - run: npm run lint:prettier - - - name: Run Tests - run: npm test -- --runInBand + - name: Install deps + run: npm ci - name: Check NPM token works run: npm whoami --registry=https://registry.npmjs.org/ diff --git a/.gitignore b/.gitignore index 3091757..a4916e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -coverage \ No newline at end of file +coverage +dist \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a0e31..989db9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## [1.9.0-beta.2](https://github.com/volbrene/githooks/compare/v1.9.0-beta.1...v1.9.0-beta.2) (2025-09-18) + +### Bug Fixes + +* ensure 'prepare' script is set in package.json for init command ([9c3677d](https://github.com/volbrene/githooks/commit/9c3677d2da42d26a5ffebf36621562e25f34ab36)) + +## [1.9.0-beta.1](https://github.com/volbrene/githooks/compare/v1.8.1...v1.9.0-beta.1) (2025-09-18) + +### Features + +* add build to pretest ([52d600a](https://github.com/volbrene/githooks/commit/52d600ab6a7ffec9328ddcbe35927efa8c1c5f7d)) +* aktualisiere CI- und Release-Workflows zur Vereinfachung der Trigger-Bedingungen ([85b9715](https://github.com/volbrene/githooks/commit/85b97151a1f5dd895a62c2e45c73a6aaf0121994)) +* erweitere Branch-Konfiguration in release.config.cjs ([31787f1](https://github.com/volbrene/githooks/commit/31787f1143c31c393290ce78c92c4cce0f65c06e)) +* fix uninstall test ([1838177](https://github.com/volbrene/githooks/commit/1838177e95d4991c8a8cf39ca49a2f202d4c7ba4)) +* init first cli tests ([1dd2632](https://github.com/volbrene/githooks/commit/1dd2632cd1f927b888f5d55b56f013248300df5a)) +* init typescript ([ea6dfa4](https://github.com/volbrene/githooks/commit/ea6dfa49d41f17ed414109d516ef987958d171c6)) +* refactoring src ([452a7b6](https://github.com/volbrene/githooks/commit/452a7b69d74cbfb534ebeacf03073a51b4f59385)) +* remove old scripts ([a3b5ed9](https://github.com/volbrene/githooks/commit/a3b5ed97c681da89e512d91ec1641f08072cc0a7)) +* simplify CLI command names in README ([f2167b4](https://github.com/volbrene/githooks/commit/f2167b450e076e307161eccd28e129724b94491d)) +* update Readme ([de52308](https://github.com/volbrene/githooks/commit/de523086923eceaf0abe22dc01e4fede197930d8)) +* update section header for CLI commands in README ([1915e50](https://github.com/volbrene/githooks/commit/1915e50656c91b77b89d665d29f43ccd6c94f455)) + ## [1.8.1](https://github.com/volbrene/githooks/compare/v1.8.0...v1.8.1) (2025-09-16) ### Bug Fixes diff --git a/README.md b/README.md index 0e56a12..c1fbae0 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![npm version](https://img.shields.io/npm/v/volbrene-git-hooks.svg)](https://www.npmjs.com/package/volbrene-git-hooks) [![CI](https://github.com/volbrene/githooks/actions/workflows/ci.yml/badge.svg)](https://github.com/volbrene/githooks/actions) +[![npm downloads](https://img.shields.io/npm/dm/volbrene-git-hooks.svg)](https://www.npmjs.com/package/volbrene-git-hooks) > **Volbrene – Git Hooks** helps you keep your commit messages consistent and enforce [Conventional Commits](https://www.conventionalcommits.org/) automatically. @@ -18,13 +19,27 @@ ## πŸ“¦ Installation -Make sure you have [Node.js](https://nodejs.org/) and `npm` installed, then run: +Make sure you have [Node.js](https://nodejs.org/) (>=16) and `npm` installed, then run: ```sh npm install --save-dev volbrene-git-hooks ``` -The preinstall script in this package will automatically place the hooks into .git/hooks/. +After that, initialize the hooks with: + +```sh +npx volbrene-git-hooks init +``` + +This will add a `prepare` script to your `package.json`: + +```jsonc +"scripts": { + "prepare": "volbrene-git-hooks" +} +``` + +With this in place, the hooks will be automatically reinstalled whenever you (or your team) run `npm install`. ## πŸ”— Available Hooks @@ -62,21 +77,40 @@ Supported branch prefixes: | `revert/*` | `revert(...)` | | `task/*` or unknown | `chore(...)` | -# CLI Commands +# βš™οΈ CLI Commands -## `volbrene-git-hooks install` +## `install` Installs or re-installs the Git hooks for the current repository. - Ensures `.git/hooks` exists - Copies the `prepare-commit-msg` hook from this package into `.git/hooks` -- Makes the hook executable on Linux/macOS -- Can be run manually or in your `package.json` (`prepare` or `postinstall` script) -## `volbrene-git-hooks reset-hooks` +## `reset-hooks` Resets Git’s `core.hooksPath` back to the default `.git/hooks` folder. - Unsets any custom `core.hooksPath` (e.g. from Husky or other tools) - Sets the local repository back to `.git/hooks` - Prints the effective hook directory for verification + +## `uninstall` + +Removes all installed Git hooks and unsets `core.hooksPath`. + +- Deletes the `.git/hooks` folder (or the directory configured in `core.hooksPath`) +- Attempts to unset `core.hooksPath` (ignored if not set) +- Useful for clean-up or before switching to another hook manager + +## `init` + +Sets up automatic hook installation on `npm install`. + +- Adds `"prepare": "volbrene-git-hooks"` to your `package.json` scripts +- Ensures hooks will be installed for every developer after `npm install` +- Recommended for teams to keep hooks consistent +- Copies the `prepare-commit-msg` hook from this package into `.git/hooks` + +## `help` + +Shows usage information and a list of available commands. diff --git a/package-lock.json b/package-lock.json index 9e5f58c..d9b0498 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "volbrene-git-hooks", - "version": "1.7.4", + "version": "1.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "volbrene-git-hooks", - "version": "1.7.4", - "hasInstallScript": true, + "version": "1.8.1", "license": "MIT", "bin": { - "volbrene-git-hooks": "scripts/cli.js" + "volbrene-git-hooks": "dist/cli.js" }, "devDependencies": { "@semantic-release/changelog": "^6.0.3", @@ -18,10 +17,11 @@ "@semantic-release/github": "^10.0.5", "@semantic-release/npm": "^12.0.0", "@types/jest": "^29.5.14", - "@types/node": "^22.18.4", + "@types/node": "^22.18.6", "conventional-changelog-conventionalcommits": "^9.1.0", "jest": "^29.7.0", "prettier": "^3.6.2", + "rimraf": "^6.0.1", "semantic-release": "^24.0.0", "ts-jest": "^29.4.2", "ts-node": "^10.9.2", @@ -582,6 +582,119 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1902,9 +2015,9 @@ } }, "node_modules/@types/node": { - "version": "22.18.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.4.tgz", - "integrity": "sha512-UJdblFqXymSBhmZf96BnbisoFIr8ooiiBRMolQgg77Ea+VM37jXw76C2LQr9n8wm9+i/OvlUlW6xSvqwzwqznw==", + "version": "22.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", + "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2943,6 +3056,13 @@ "readable-stream": "^2.0.2" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.218", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", @@ -3386,6 +3506,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -4109,6 +4259,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/java-properties": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", @@ -5162,6 +5328,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8119,6 +8295,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8225,6 +8408,33 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -8733,6 +8943,66 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9612,6 +9882,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -9625,6 +9911,30 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -10285,6 +10595,25 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index b90ac4f..803e51a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "volbrene-git-hooks", - "version": "1.8.1", + "version": "1.9.0-beta.2", + "type": "module", "description": "Git hook scripts with Conventional Commits enforcement", "author": "Rene Volbach", "license": "MIT", @@ -12,16 +13,23 @@ "node": ">=16" }, "scripts": { - "preinstall": "node scripts/init-hooks.js", + "build": "tsc", + "clean": "rimraf dist", + "prepare": "npm run build", + "prepack": "npm run build", + "prepublishOnly": "npm run build && chmod +x dist/cli.js", + "postbuild": "chmod +x dist/cli.js", + "pretest": "npm run build", "test": "jest --runInBand", "lint:prettier": "prettier --check .", "format": "prettier --write .", "release": "semantic-release" }, "bin": { - "volbrene-git-hooks": "scripts/cli.js" + "volbrene-git-hooks": "dist/cli.js" }, "files": [ + "dist", "hooks", "scripts" ], @@ -39,10 +47,11 @@ "@semantic-release/github": "^10.0.5", "@semantic-release/npm": "^12.0.0", "@types/jest": "^29.5.14", - "@types/node": "^22.18.4", + "@types/node": "^22.18.6", "conventional-changelog-conventionalcommits": "^9.1.0", "jest": "^29.7.0", "prettier": "^3.6.2", + "rimraf": "^6.0.1", "semantic-release": "^24.0.0", "ts-jest": "^29.4.2", "ts-node": "^10.9.2", diff --git a/release.config.cjs b/release.config.cjs index eb79508..e1942b3 100644 --- a/release.config.cjs +++ b/release.config.cjs @@ -1,5 +1,5 @@ module.exports = { - branches: ['main'], + branches: ['main', { name: 'develop', prerelease: 'beta' }, { name: 'rc', prerelease: 'rc' }], preset: 'conventionalcommits', plugins: [ '@semantic-release/commit-analyzer', diff --git a/scripts/cli.js b/scripts/cli.js deleted file mode 100755 index 27dd524..0000000 --- a/scripts/cli.js +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env node -const { execSync } = require('child_process'); -const path = require('path'); - -const args = process.argv.slice(2); -const command = args[0] || ''; - -function sh(cmd, opts = {}) { - return execSync(cmd, { stdio: 'inherit', ...opts }); -} - -switch (command) { - case 'reset-hooks': - console.log('πŸ”§ Resetting core.hooksPath to .git/hooks...'); - try { - try { - sh('git config --unset core.hooksPath'); - } catch {} - sh('git config --local core.hooksPath .git/hooks'); - const output = execSync('git rev-parse --git-path hooks').toString().trim(); - console.log(`βœ… core.hooksPath is now: ${output}`); - } catch (e) { - console.error(`❌ Failed to reset hooks: ${e.message}`); - process.exit(1); - } - break; - - case 'install': - console.log('πŸ”— Installing hooks via init-hooks.js...'); - try { - const initPath = path.resolve(__dirname, 'init-hooks.js'); - sh(`node ${JSON.stringify(initPath)}`); - console.log('βœ… init-hooks.js executed successfully'); - } catch (e) { - console.error(`❌ Failed to execute init-hooks.js: ${e.message}`); - process.exit(1); - } - break; - - default: - console.log(`ℹ️ Unknown command: ${command}`); - console.log('Usage: volbrene-git-hooks [reset-hooks|install]'); - process.exit(1); -} diff --git a/scripts/init-hooks.js b/scripts/init-hooks.js deleted file mode 100644 index c539105..0000000 --- a/scripts/init-hooks.js +++ /dev/null @@ -1,85 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const os = require('os'); -const { exec, execSync } = require('child_process'); - -console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); -console.log('πŸ”§ Git Hooks Setup'); -console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - -////////////////////// Check node Version /////////////////// -let ver = process.versions.node; -ver = ver.split('.')[0]; -if (ver < 6) { - console.error(`❌ Node.js >= 6 required. Detected: ${process.version}`); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - process.exit(1); -} - -////////////////////// FUNCTIONS /////////////////// - -// add git hooks -let addGitHook = (hookName, sourcePath, targetPath) => { - const data = fs.readFileSync(path.join(sourcePath, hookName)); - - try { - fs.mkdirSync(targetPath); - } catch { - // folder already exists β†’ ignore - } - - if (!fs.existsSync(path.join(targetPath, hookName))) { - fs.writeFileSync(path.join(targetPath, hookName), data); - - // Make pre-commit hook executable on linux and mac - if (os.platform() === 'linux' || os.platform() === 'darwin') { - exec('chmod +x ' + hookName, { cwd: path.join(targetPath) }, function (err, stdout) { - if (err) console.error(`❌ chmod failed: ${err.message}`); - if (stdout.trim()) console.log(`ℹ️ chmod output: ${stdout.trim()}`); - }); - } - - console.log(`βœ… Hook "${hookName}" added to ${targetPath}`); - } else { - console.log(`ℹ️ Hook "${hookName}" already exists in ${targetPath}`); - } -}; - -////////////////////// INIT /////////////////// -let gitPath = process.env.INIT_CWD; - -console.log('πŸ“ Resolving repository root...'); -if ( - !fs.existsSync(`${process.env.INIT_CWD}/.git`) && - fs.existsSync(`${process.env.INIT_CWD}/../.git`) -) { - gitPath = `${process.env.INIT_CWD}/../`; -} - -// ensure we are in a git repo -if (!fs.existsSync(path.join(gitPath, '.git'))) { - console.log('⚠️ No git repository found. Skipping hook setup.'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - process.exit(0); -} - -console.log('🧹 Removing old hooks folder if it exists...'); -try { - fs.rmSync(path.join(gitPath, '.git', 'hooks'), { recursive: true, force: true }); - console.log('βœ… Old hooks folder removed'); -} catch { - console.log('ℹ️ No old hooks folder found'); -} - -console.log('πŸ”— Installing prepare-commit-msg hook...'); -try { - const hooksSourceDir = path.resolve(__dirname, '../hooks'); // <== nutzt Pfad der Datei - console.log(`πŸ“‚ Using hooks source dir: ${hooksSourceDir}`); - - addGitHook('prepare-commit-msg', hooksSourceDir, path.join(gitPath, '.git', 'hooks')); -} catch (e) { - console.error(`❌ Failed to add hook: ${e.message}`); -} - -console.log('βœ… Git Hooks setup completed'); -console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..cc4e25a --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env node +import { Command } from './types.js'; +import { log } from './utils/log.js'; +import { handleResetHooks } from './commands/resetHooks.js'; +import { handleInstall } from './commands/install.js'; +import { handleUninstall } from './commands/uninstall.js'; +import { handleInit } from './commands/init.js'; + +function printUsage(): void { + log.info('Usage: volbrene-git-hooks \n'); + log.info('Commands:'); + log.info(' init Add prepare script and install hooks'); + log.info(' reset-hooks Reset core.hooksPath to .git/hooks'); + log.info(' install Install hooks'); + log.info(' uninstall Remove hooks folder and unset core.hooksPath'); +} + +(function main() { + const [, , ...argv] = process.argv; + const command = (argv[0] || 'install') as Command; + + switch (command) { + case 'init': + handleInit(); + break; + case 'reset-hooks': + handleResetHooks(); + break; + case 'install': + handleInstall(); + break; + case 'uninstall': + handleUninstall(); + break; + case 'help': + case '': + default: + if (command && command !== 'help') { + log.info(`ℹ️ Unknown command: ${command}`); + } + printUsage(); + process.exit(command ? 1 : 0); + } +})(); diff --git a/src/commands/init.ts b/src/commands/init.ts new file mode 100644 index 0000000..32c0017 --- /dev/null +++ b/src/commands/init.ts @@ -0,0 +1,37 @@ +import fs from 'node:fs'; +import { handleInstall } from './install.js'; +import { resetHooksPath } from '../utils/git.js'; +import { log } from '../utils/log.js'; + +/** + * Handles the 'init' command. + */ +export function handleInit(): void { + const packageFile = 'package.json'; + const raw = fs.readFileSync(packageFile, 'utf8'); + const pkg = JSON.parse(raw) as Record; + + // Ensure scripts object exists + pkg.scripts = pkg.scripts || {}; + + const prepareScript = pkg.scripts.prepare; + const cliCommand = 'volbrene-git-hooks'; + + if (!prepareScript) { + pkg.scripts.prepare = cliCommand; + log.ok(`Added "prepare": "${cliCommand}" to package.json`); + } else if (!prepareScript.includes(cliCommand)) { + pkg.scripts.prepare = `${prepareScript} && ${cliCommand}`; + log.step(`Updated existing "prepare" script to also run "${cliCommand}"`); + } + + // Preserve formatting (tab vs spaces) + const indent = /\t/.test(raw) ? '\t' : 2; + fs.writeFileSync(packageFile, JSON.stringify(pkg, null, indent) + '\n'); + + // set correct hooks path before installing + resetHooksPath(); + + // Immediately install hooks + handleInstall(); +} diff --git a/src/commands/install.ts b/src/commands/install.ts new file mode 100644 index 0000000..8f40dfb --- /dev/null +++ b/src/commands/install.ts @@ -0,0 +1,88 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { assertGitRepo, getHooksPath, resolveRepoRoot } from '../utils/git.js'; +import { log } from '../utils/log.js'; +import { fail } from '../utils/errors.js'; + +/** + * + * @param hookName + * @param sourceDir + * @param targetDir + */ +function addGitHook(hookName: string, sourceDir: string, targetDir: string): void { + const srcFile = path.join(sourceDir, hookName); + const dstFile = path.join(targetDir, hookName); + + // Read as text to preserve shebang/newlines predictably + const sourceContent = fs.readFileSync(srcFile, 'utf8'); + + // Ensure hooks directory exists + fs.mkdirSync(targetDir, { recursive: true }); + + let shouldWrite = true; + + if (fs.existsSync(dstFile)) { + try { + const existingContent = fs.readFileSync(dstFile, 'utf8'); + if (existingContent === sourceContent) { + log.info(`ℹ️ Hook "${hookName}" already exists and is up-to-date – skipping.`); + shouldWrite = false; + } else { + log.info(`πŸ”„ Hook "${hookName}" exists but differs – updating...`); + } + } catch (e) { + log.info(`ℹ️ Could not read existing hook, will overwrite. Reason: ${(e as Error).message}`); + } + } + + if (shouldWrite) { + fs.writeFileSync(dstFile, sourceContent, 'utf8'); + + // Make hook executable on Linux/macOS + if (os.platform() === 'linux' || os.platform() === 'darwin') { + try { + fs.chmodSync(dstFile, 0o755); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + log.info(`ℹ️ chmod failed (non-fatal): ${msg}`); + } + } + + log.ok(`Hook "${hookName}" written to ${targetDir}`); + } +} + +/** + * Handles the 'install' command. + * + * @returns void + */ +export function handleInstall(): void { + assertGitRepo(); + + log.step('Git Hooks Setup'); + + // Resolve repo root + const repoRoot = resolveRepoRoot(); + + if (!fs.existsSync(path.join(repoRoot, '.git'))) { + log.info('⚠️ No git repository found. Skipping hook setup.'); + return; + } + + // Determine hooks source dir relative to this module (ESM-safe) + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const hooksSourceDir = path.resolve(moduleDir, '../../hooks'); + log.info(`πŸ“‚ Using hooks source dir: ${hooksSourceDir}`); + + // Copy hook + try { + const targetHooksDir = getHooksPath(); + addGitHook('prepare-commit-msg', hooksSourceDir, targetHooksDir); + } catch (e) { + fail(e, 'Failed to add hook'); + } +} diff --git a/src/commands/resetHooks.ts b/src/commands/resetHooks.ts new file mode 100644 index 0000000..353654a --- /dev/null +++ b/src/commands/resetHooks.ts @@ -0,0 +1,21 @@ +import { assertGitRepo, getHooksPath, resetHooksPath } from '../utils/git.js'; +import { log } from '../utils/log.js'; +import { fail } from '../utils/errors.js'; + +/** + * Handles the 'reset-hooks' command. + */ +export function handleResetHooks(): void { + assertGitRepo(); + + log.step('Resetting core.hooksPath to .git/hooks...'); + + try { + resetHooksPath(); + + const output = getHooksPath(); + log.ok(`core.hooksPath is now: ${output}`); + } catch (e) { + fail(e, 'Failed to reset hooks'); + } +} diff --git a/src/commands/uninstall.ts b/src/commands/uninstall.ts new file mode 100644 index 0000000..1a2899c --- /dev/null +++ b/src/commands/uninstall.ts @@ -0,0 +1,20 @@ +import { assertGitRepo, removeHooksDirAndUnset } from '../utils/git.js'; +import { log } from '../utils/log.js'; +import { fail } from '../utils/errors.js'; + +/** + * Handles the 'uninstall' command. + */ +export function handleUninstall(): void { + assertGitRepo(); + + log.link('Uninstalling hooks...'); + + try { + removeHooksDirAndUnset(); + + log.ok('hooks uninstalled successfully'); + } catch (e) { + fail(e, 'Failed to uninstall hooks'); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..4266d99 --- /dev/null +++ b/src/types.ts @@ -0,0 +1 @@ +export type Command = 'init' | 'reset-hooks' | 'install' | 'uninstall' | 'help' | ''; diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..a200d72 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,8 @@ +import { log } from './log.js'; + +/** Exit with error in a uniform way */ +export function fail(e: unknown, prefix: string): never { + if (e instanceof Error) log.error(`${prefix}: ${e.message}`); + else log.error(`${prefix}: Unknown error`); + process.exit(1); +} diff --git a/src/utils/exec.ts b/src/utils/exec.ts new file mode 100644 index 0000000..2fc3004 --- /dev/null +++ b/src/utils/exec.ts @@ -0,0 +1,13 @@ +import { execSync, ExecSyncOptions } from 'node:child_process'; + +/** Run a command and stream stdio to current process (no output returned). */ +export function sh(cmd: string, opts: ExecSyncOptions = {}): void { + execSync(cmd, { stdio: 'inherit', ...opts }); +} + +/** Run a command and capture its stdout (no live output). */ +export function shGetOutput(cmd: string, opts: ExecSyncOptions = {}): string { + return execSync(cmd, { stdio: 'pipe', ...opts }) + .toString() + .trim(); +} diff --git a/src/utils/git.ts b/src/utils/git.ts new file mode 100644 index 0000000..4d74ab2 --- /dev/null +++ b/src/utils/git.ts @@ -0,0 +1,58 @@ +import fs from 'node:fs'; +import { sh, shGetOutput } from './exec.js'; +import { fail } from './errors.js'; +import path from 'node:path'; + +/** Ensure we are inside a git repository (cheap sanity check). */ +export function assertGitRepo(): void { + try { + shGetOutput('git rev-parse --is-inside-work-tree'); + } catch (e) { + fail(e, 'Not a git repository'); + } +} + +/** + * Resolve repository root (prefers Git, falls back to INIT_CWD heuristic). + */ +export function resolveRepoRoot(): string { + try { + const top = shGetOutput('git rev-parse --show-toplevel'); + if (top) return top; + } catch { + // ignore + } + + const initCwd = process.env.INIT_CWD || process.cwd(); + if ( + !fs.existsSync(path.join(initCwd, '.git')) && + fs.existsSync(path.join(initCwd, '..', '.git')) + ) { + return path.resolve(initCwd, '..'); + } + return initCwd; +} + +/** Get the effective hooks path (respects core.hooksPath). */ +export function getHooksPath(): string { + return shGetOutput('git rev-parse --git-path hooks'); +} + +/** Reset core.hooksPath to .git/hooks */ +export function resetHooksPath(): void { + // Unset core.hooksPath if present (ignore failures) + try { + sh('git config --unset core.hooksPath'); + } catch {} + // Set local hooks path back to .git/hooks + sh('git config --local core.hooksPath .git/hooks'); +} + +/** Remove hooks dir and unset core.hooksPath (ignore unset errors). */ +export function removeHooksDirAndUnset(): void { + const hooksPath = getHooksPath(); + fs.rmSync(hooksPath, { recursive: true, force: true }); + try { + sh('git config --unset core.hooksPath'); + } catch {} +} diff --git a/src/utils/log.ts b/src/utils/log.ts new file mode 100644 index 0000000..f441357 --- /dev/null +++ b/src/utils/log.ts @@ -0,0 +1,10 @@ +/* eslint-disable no-console */ + +/** Simple logging utility with different log levels and icons. */ +export const log = { + info: (msg: string) => console.log(msg), + ok: (msg: string) => console.log(`βœ… ${msg}`), + step: (msg: string) => console.log(`πŸ”§ ${msg}`), + link: (msg: string) => console.log(`πŸ”— ${msg}`), + error: (msg: string) => console.error(`❌ ${msg}`), +}; diff --git a/tests/_utils/utils.ts b/tests/_utils/utils.ts new file mode 100644 index 0000000..0af4e7d --- /dev/null +++ b/tests/_utils/utils.ts @@ -0,0 +1,21 @@ +import { execSync } from 'node:child_process'; +import path from 'node:path'; +import * as fs from 'node:fs'; + +export function sh(cmd: string, cwd: string): string { + return execSync(cmd, { cwd, stdio: 'pipe' }).toString().trim(); +} + +export function fileExists(p: string): boolean { + try { + fs.accessSync(p, fs.constants.F_OK); + return true; + } catch { + return false; + } +} + +export const getHooksDir = (cwd: string) => { + const raw = sh('git rev-parse --git-path hooks', cwd).trim(); + return path.isAbsolute(raw) ? raw : path.resolve(cwd, raw); +}; diff --git a/tests/cli.test.ts b/tests/cli.test.ts new file mode 100644 index 0000000..246c63d --- /dev/null +++ b/tests/cli.test.ts @@ -0,0 +1,113 @@ +import { execSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { fileExists, getHooksDir, sh } from './_utils/utils'; + +const NODE = process.execPath; +const CLI = path.resolve('dist/cli.js'); + +function setupGitRepo(): { cwd: string; hooksDir: string } { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'pcm-')); + + sh('git init -q', cwd); + sh('git config user.name "Test"', cwd); + sh('git config user.email "test@example.com"', cwd); + fs.writeFileSync(path.join(cwd, 'README.md'), 'init'); + sh('git add README.md', cwd); + sh('git commit -qm "chore: init"', cwd); + + const hooksDir = sh('git rev-parse --git-path hooks', cwd); + + return { cwd, hooksDir }; +} + +function runCLI(args: string[], cwd: string): string { + try { + return execSync(`${NODE} ${JSON.stringify(CLI)} ${args.join(' ')}`, { + cwd, + stdio: 'pipe', + }).toString(); + } catch (e: any) { + return (e?.stdout?.toString?.() || '') + (e?.stderr?.toString?.() || ''); + } +} + +describe('volbrene-git-hooks CLI', () => { + test('init writes prepare script into package.json (no install)', () => { + const { cwd } = setupGitRepo(); + + // create minimal package.json + const pkgPath = path.join(cwd, 'package.json'); + fs.writeFileSync( + pkgPath, + JSON.stringify({ name: 'tmp', version: '1.0.0', scripts: {} }, null, 2) + ); + + const out = runCLI(['init'], cwd); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + expect(pkg.scripts.prepare).toBe('volbrene-git-hooks'); + expect(out).toMatch(/Git Hooks Setup/); + }); + + test('init hooks', () => { + const { cwd } = setupGitRepo(); + + const out = runCLI([], cwd); + + const hooksDir = getHooksDir(cwd); + expect(fileExists(path.join(hooksDir, 'prepare-commit-msg'))).toBe(true); + + expect(out).toContain('πŸ”§ Git Hooks Setup'); + }); + test('install installs (idempotent)', () => { + const { cwd } = setupGitRepo(); + + // first install + const out1 = runCLI(['install'], cwd); + let hooksDir = getHooksDir(cwd); + expect(fileExists(path.join(hooksDir, 'prepare-commit-msg'))).toBe(true); + expect(out1).toContain('πŸ”§ Git Hooks Setup'); + + // second install + const out2 = runCLI(['install'], cwd); + hooksDir = getHooksDir(cwd); + expect(fileExists(path.join(hooksDir, 'prepare-commit-msg'))).toBe(true); + expect(out2).toMatch(/up-to-date|already exists/i); + }); + + test('reset-hooks sets core.hooksPath back to .git/hooks', () => { + const { cwd } = setupGitRepo(); + + // first set hooksPath to something else + sh('git config core.hooksPath "temp"', cwd); + + // then reset via CLI + const out = runCLI(['reset-hooks'], cwd); + + const effective = sh('git rev-parse --git-path hooks', cwd); + expect(effective).toMatch(/.git\/hooks/i); + expect(out).toMatch(/Resetting core.hooksPath/i); + }); + + test('uninstall removes hooks and unsets hooksPath', () => { + const { cwd } = setupGitRepo(); + + runCLI(['install'], cwd); + + const hooksDir = getHooksDir(cwd); + expect(fileExists(path.join(hooksDir, 'prepare-commit-msg'))).toBe(true); + + const out = runCLI(['uninstall'], cwd); + + const hooksDirAfter = getHooksDir(cwd); + expect(fileExists(path.join(hooksDirAfter, 'prepare-commit-msg'))).toBe(false); + expect(out).toMatch(/uninstalled/i); + }); + + test('help prints usage', () => { + const { cwd } = setupGitRepo(); + const out = runCLI(['help'], cwd); + expect(out).toMatch(/Usage: volbrene-git-hooks/i); + }); +}); diff --git a/tests/prepare-commit-msg.test.ts b/tests/prepare-commit-msg.test.ts index 54c7947..32be2bf 100644 --- a/tests/prepare-commit-msg.test.ts +++ b/tests/prepare-commit-msg.test.ts @@ -1,26 +1,23 @@ -import { execSync, spawnSync } from 'node:child_process'; +import { spawnSync } from 'node:child_process'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; +import { sh } from './_utils/utils'; const HOOK_PATH = process.env.HOOK_PATH || path.resolve(process.cwd(), 'hooks/prepare-commit-msg'); const BASH = process.env.BASH || 'bash'; -function run(cmd: string, cwd: string) { - return execSync(cmd, { cwd, stdio: 'pipe' }).toString(); -} - function setupRepo(): { workdir: string; msgFile: string } { const workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'pcm-')); fs.copyFileSync(HOOK_PATH, path.join(workdir, 'prepare-commit-msg')); fs.chmodSync(path.join(workdir, 'prepare-commit-msg'), 0o755); - run('git init -q', workdir); - run('git config user.name "Test"', workdir); - run('git config user.email "test@example.com"', workdir); + sh('git init -q', workdir); + sh('git config user.name "Test"', workdir); + sh('git config user.email "test@example.com"', workdir); fs.writeFileSync(path.join(workdir, 'README.md'), 'init'); - run('git add README.md', workdir); - run('git commit -qm "chore: init"', workdir); + sh('git add README.md', workdir); + sh('git commit -qm "chore: init"', workdir); const msgFile = path.join(workdir, 'COMMIT_MSG.txt'); return { workdir, msgFile }; @@ -43,9 +40,9 @@ function runHook( ): string { // Create (or switch to) the branch try { - run(`git checkout -qb "${branch}"`, workdir); + sh(`git checkout -qb "${branch}"`, workdir); } catch { - run(`git checkout "${branch}"`, workdir); + sh(`git checkout "${branch}"`, workdir); } // Write the raw commit message to the temp file fs.writeFileSync(msgFile, raw, 'utf8'); diff --git a/tsconfig.json b/tsconfig.json index 954f7f0..5acec80 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,11 @@ { "compilerOptions": { - "target": "ES2019", - "module": "CommonJS", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", "outDir": "dist", - "rootDir": "src", "strict": true, - "esModuleInterop": false, - "forceConsistentCasingInFileNames": true + "esModuleInterop": true }, "include": ["src"] }