├── .husky ├── .gitignore └── pre-commit ├── bin ├── run.cmd └── run ├── .gitignore ├── .eslintrc ├── .editorconfig ├── .github ├── workflows │ ├── semantic-pr-title.yml │ ├── cancel.yml │ ├── lint.yml │ └── release.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── LICENSE ├── release.config.js ├── package.json ├── README.md └── src └── index.js /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('..').run() 4 | .catch(require('@oclif/errors/handle')) 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /package-lock.json 6 | /tmp 7 | node_modules 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2021 4 | }, 5 | "extends": "oclif", 6 | "rules": { 7 | "no-template-curly-in-string": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Semantic PR title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Check PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v3.0.0 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/cancel.yml: -------------------------------------------------------------------------------- 1 | name: Cancel previous runs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | 10 | jobs: 11 | cancel: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 3 14 | steps: 15 | - uses: styfle/cancel-workflow-action@0.6.0 16 | with: 17 | workflow_id: 8756852, 8756881 18 | access_token: ${{ secrets.GH_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | lint: 8 | name: Check code style 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: 14.x 19 | registry-url: https://registry.yarnpkg.com 20 | - name: Install 21 | run: yarn --frozen-lockfile 22 | - name: Lint 23 | run: | 24 | yarn test:other 25 | yarn test:code 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: nicolas-goudry 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Executed command 16 | 2. Received error 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: nicolas-goudry 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | - next 9 | 10 | jobs: 11 | release: 12 | name: Release to npm 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | persist-credentials: false 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: 14.x 24 | registry-url: https://registry.yarnpkg.com 25 | - name: Install dependencies 26 | run: yarn install --frozen-lockfile 27 | - name: Run semantic release 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | run: yarn release 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 G.Script 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | const presetConfig = { 2 | types: [{ 3 | type: 'feat', 4 | section: 'Features', 5 | }, { 6 | type: 'fix', 7 | section: 'Bug Fixes', 8 | }, { 9 | type: 'docs', 10 | section: 'Documentation', 11 | }, { 12 | type: 'chore', 13 | hidden: true, 14 | }, { 15 | type: 'style', 16 | hidden: true, 17 | }, { 18 | type: 'refactor', 19 | hidden: true, 20 | }, { 21 | type: 'perf', 22 | hidden: true, 23 | }, { 24 | type: 'test', 25 | hidden: true, 26 | }], 27 | } 28 | 29 | module.exports = { 30 | branches: ['main', 'next'], 31 | plugins: [ 32 | ['@semantic-release/commit-analyzer', { 33 | preset: 'conventionalcommits', 34 | presetConfig, 35 | }], 36 | ['@semantic-release/release-notes-generator', { 37 | preset: 'conventionalcommits', 38 | presetConfig, 39 | linkCompare: false, 40 | linkReferences: false, 41 | }], 42 | '@semantic-release/npm', 43 | ['@semantic-release/git', { 44 | assets: ['package.json'], 45 | message: 'chore(release): v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', 46 | }], 47 | ], 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-husky", 3 | "description": "Installing husky made easy as woof!", 4 | "version": "1.1.0", 5 | "author": "Nicolas Goudry ", 6 | "bin": { 7 | "auto-husky": "./bin/run" 8 | }, 9 | "bugs": "https://github.com/g-script/auto-husky/issues", 10 | "dependencies": { 11 | "@oclif/command": "1.8.0", 12 | "@oclif/config": "1.17.0", 13 | "@oclif/plugin-help": "3.2.2", 14 | "husky": "6.0.0", 15 | "inquirer": "8.0.0" 16 | }, 17 | "devDependencies": { 18 | "@oclif/dev-cli": "1.26.0", 19 | "@semantic-release/git": "9.0.0", 20 | "conventional-changelog-conventionalcommits": "4.6.0", 21 | "eslint": "7.26.0", 22 | "eslint-config-oclif": "3.1.0", 23 | "lint-staged": "11.0.0", 24 | "prettier": "2.3.0", 25 | "semantic-release": "17.4.3", 26 | "shx": "0.3.3" 27 | }, 28 | "engines": { 29 | "node": ">=10.1.0" 30 | }, 31 | "files": [ 32 | "/bin", 33 | "/src" 34 | ], 35 | "homepage": "https://github.com/g-script/auto-husky", 36 | "keywords": [ 37 | "auto-husky", 38 | "oclif", 39 | "husky", 40 | "git", 41 | "hooks", 42 | "pre-commit" 43 | ], 44 | "license": "MIT", 45 | "main": "src/index.js", 46 | "oclif": { 47 | "bin": "auto-husky" 48 | }, 49 | "repository": "g-script/auto-husky", 50 | "scripts": { 51 | "eslint": "eslint --max-warnings=0 --ignore-path=.gitignore", 52 | "fix": "yarn fix:other && yarn fix:code", 53 | "fix:code": "yarn test:code --fix", 54 | "fix:other": "yarn test:other --write", 55 | "lint-staged": "lint-staged", 56 | "prepare": "husky install && shx rm -rf .git/hooks && shx ln -s ../.husky .git/hooks", 57 | "prettier": "prettier --ignore-path=.gitignore", 58 | "release": "semantic-release", 59 | "test": "yarn test:code && yarn test:other", 60 | "test:code": "yarn eslint --ext .js .", 61 | "test:other": "yarn prettier --list-different \"**/*.{json,md,yml}\"" 62 | }, 63 | "lint-staged": { 64 | "*.js": "yarn eslint --fix", 65 | "*.{json,md,yml}": "yarn prettier --write" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # auto-husky 4 | 5 | Installing husky made easy as woof! 🐶 6 | 7 | [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) 8 | [![Version](https://img.shields.io/npm/v/auto-husky.svg)](https://npmjs.org/package/auto-husky) 9 | [![Downloads/week](https://img.shields.io/npm/dw/auto-husky.svg)](https://npmjs.org/package/auto-husky) 10 | [![License](https://img.shields.io/npm/l/auto-husky.svg)](https://github.com/g-script/auto-husky/blob/main/package.json) 11 | 12 | 13 | 14 | ## :bookmark_tabs: Table of contents 15 | 16 | - [:floppy_disk:Installation](#floppy_diskinstallation) 17 | - [:beginner: Usage](#beginner-usage) 18 | - [:1234: Versioning](#1234-versioning) 19 | 20 | ## :floppy_disk:Installation 21 | 22 | You can install the package globally if you need it regularly: 23 | 24 | ```shell 25 | $ npm install -g auto-husky 26 | ``` 27 | 28 | Or you can run it directly with `npx`: 29 | 30 | ```shell 31 | $ npx auto-husky 32 | ``` 33 | 34 | ## :beginner: Usage 35 | 36 | This package can be used in fully interactive mode or by specifying some options. 37 | 38 | There is only one argument to provide: **WORKINGDIRECTORY**. This is the directory where command will be executed, it should point to the directory under which `.git` folder is located. It defaults to current working directory, and supports relative paths. 39 | 40 | There is also a few flags available: 41 | 42 | **`--interactive`** (`-i`) 43 | 44 | Turn on interactive mode. 45 | 46 | This option will interactively ask you questions matching following flags. You can preset all answers through matching flags, but only boolean flags will not be asked again. 47 | 48 | **`--destination`** (`-d`) 49 | 50 | Set a custom installation directory for husky. 51 | 52 | This should point to the directory where your `package.json` file is located. It defaults to working directory and must be set as relative to it. 53 | 54 | **`--[no-]yarn2`** 55 | 56 | Setup husky for yarn 2. It will use `postinstall` script rather than `prepare` script, which is not supported by yarn 2. 57 | 58 | **`--[no-]pinst`** (`-p`) 59 | 60 | Install and setup [pinst](https://www.npmjs.com/package/pinst). 61 | 62 | This option will add two scripts (`prepublishOnly` and `postpublish`) that will disable `postinstall` script when publishing your package to a registry. 63 | 64 | > **This is only useful for yarn 2 projects!** 65 | > It is not needed with npm or yarn because they do not use `postinstall` script to automatically install husky. 66 | 67 | **`--[no-]fix-gitkraken`** (`-g`) 68 | 69 | Automatically apply [compatibility fix for Gitkraken](https://github.com/typicode/husky/issues/875). 70 | 71 | **Examples:** 72 | 73 | ```shell 74 | # Most common usage 75 | $ auto-husky 76 | 77 | # Fully interactive usage 78 | $ auto-husky -i 79 | 80 | # Preset some answers for interactive mode 81 | $ auto-husky -i --no-pinst 82 | 83 | # Usage with custom folder 84 | $ auto-husky -d ./custom-folder 85 | ``` 86 | 87 | ## :1234: Versioning 88 | 89 | This project uses [SemVer](http://semver.org) for versioning. For the versions available, see the [tags on this repository](https://github.com/g-script/auto-husky/tags). 90 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const {Command, flags: oFlags} = require('@oclif/command') 2 | const fs = require('fs') 3 | const husky = require('husky') 4 | const inquirer = require('inquirer') 5 | const path = require('path') 6 | 7 | const log = message => { 8 | console.log(`auto-husky - ${message}`) 9 | } 10 | 11 | const CURRENT_WORKING_DIRECTORY = process.cwd() 12 | 13 | class HuskyInstallCommand extends Command { 14 | /** 15 | * Check working directory exists and is a Git repository 16 | * @param {String} workingDirectory path to working directory 17 | * @returns {Boolean|String} true if value is valid, error message otherwise 18 | */ 19 | checkWorkingDirectory(workingDirectory) { 20 | if (!fs.existsSync(workingDirectory)) { 21 | return `Working directory is invalid (missing directory or invalid permissions): ${workingDirectory}` 22 | } 23 | 24 | const gitDir = path.join(workingDirectory, '.git') 25 | 26 | if (!fs.existsSync(gitDir)) { 27 | return 'Working directory is not a Git repository' 28 | } 29 | 30 | return true 31 | } 32 | 33 | /** 34 | * Check installation directory exists 35 | * @param {String} installDirectory path to installation directory 36 | * @returns {Boolean|String} true if value is valid, error message otherwise 37 | */ 38 | checkInstallDirectory(installDirectory) { 39 | if (!fs.existsSync(installDirectory)) { 40 | return `Installation directory is invalid (missing directory or invalid permissions): ${installDirectory}` 41 | } 42 | 43 | return true 44 | } 45 | 46 | /** 47 | * Main function 48 | * @returns {void} 49 | */ 50 | async run() { 51 | const {args, flags} = this.parse(HuskyInstallCommand) 52 | const defaultDestination = path.resolve(args.workingDirectory, flags.destination || '') 53 | let interactiveOptions = {} 54 | 55 | if (flags.interactive) { 56 | await inquirer.prompt([{ 57 | name: 'workingDirectory', 58 | type: 'input', 59 | message: 'Enter working directory:', 60 | default: args.workingDirectory, 61 | filter: input => path.resolve(args.workingDirectory, input), 62 | validate: this.checkWorkingDirectory, 63 | }, { 64 | name: 'destination', 65 | type: 'input', 66 | message: 'Enter installation directory:', 67 | default: answers => path.resolve(answers.workingDirectory, flags.destination || ''), 68 | filter: (input, answers) => path.resolve(answers.workingDirectory, input), 69 | validate: this.checkInstallDirectory, 70 | }, { 71 | when: flags.pinst === undefined, 72 | name: 'yarn2', 73 | type: 'confirm', 74 | message: 'Use yarn 2 (berry)?', 75 | default: Boolean(flags.yarn2), 76 | }, { 77 | when: flags.pinst === undefined, 78 | name: 'pinst', 79 | type: 'confirm', 80 | message: 'Use pinst?', 81 | default: Boolean(flags.pinst), 82 | }, { 83 | when: flags['fix-gitkraken'] === undefined, 84 | name: 'gitkrakenFix', 85 | type: 'confirm', 86 | message: 'Apply Gitkraken fix?', 87 | default: Boolean(flags['fix-gitkraken']), 88 | }]).then(answers => { 89 | interactiveOptions = answers 90 | }) 91 | } else { 92 | const wdCheck = this.checkWorkingDirectory(args.workingDirectory) 93 | 94 | if (wdCheck !== true) { 95 | throw new Error(wdCheck) 96 | } 97 | 98 | const destCheck = this.checkInstallDirectory(defaultDestination) 99 | 100 | if (destCheck !== true) { 101 | throw new Error(destCheck) 102 | } 103 | } 104 | 105 | const options = { 106 | workingDirectory: args.workingDirectory, 107 | destination: defaultDestination, 108 | yarn2: Boolean(flags.yarn2), 109 | pinst: Boolean(flags.pinst), 110 | gitkrakenFix: Boolean(flags['fix-gitkraken']), 111 | ...interactiveOptions, 112 | } 113 | 114 | log(`Installing husky into ${options.workingDirectory}`) 115 | 116 | const pkgPath = path.join(options.destination, 'package.json') 117 | const pkgContent = await fs.promises.readFile(pkgPath, { 118 | encoding: 'utf8', 119 | }) 120 | const originalPkgIndent = /^[ ]+|\t+/m.exec(pkgContent)?.[0] 121 | const pkg = JSON.parse(pkgContent) 122 | 123 | if (!pkg.devDependencies) { 124 | pkg.devDependencies = {} 125 | } 126 | 127 | pkg.devDependencies.husky = '^6.0.0' 128 | 129 | if (options.gitkrakenFix) { 130 | pkg.devDependencies.shx = '^0.3.3' 131 | } 132 | 133 | if (!pkg.scripts) { 134 | pkg.scripts = {} 135 | } 136 | 137 | const huskyDir = path.join(options.destination, '.husky') 138 | let installScript = 'husky install' 139 | 140 | if (options.destination !== options.workingDirectory) { 141 | installScript = `cd ${path.relative(options.destination, options.workingDirectory)} && husky install ${path.relative(options.workingDirectory, huskyDir)}` 142 | } 143 | 144 | if (options.gitkrakenFix) { 145 | const relativeHooksDir = path.relative(options.workingDirectory, path.join(options.workingDirectory, '.git/hooks')) 146 | 147 | installScript += ` && shx rm -rf ${relativeHooksDir} && shx ln -s ${path.relative(path.join(options.workingDirectory, '.git'), huskyDir)} ${relativeHooksDir}` 148 | } 149 | 150 | if (options.yarn2) { 151 | if (pkg.scripts.postinstall) { 152 | pkg.scripts['postinstall:old'] = pkg.scripts.postinstall 153 | } 154 | 155 | pkg.scripts.postinstall = installScript 156 | } else { 157 | if (pkg.scripts.prepare) { 158 | pkg.scripts['prepare:old'] = pkg.scripts.prepare 159 | } 160 | 161 | pkg.scripts.prepare = installScript 162 | } 163 | 164 | if (options.pinst) { 165 | if (pkg.scripts.prepublishOnly) { 166 | pkg.scripts['prepublishOnly:old'] = pkg.scripts.prepublishOnly 167 | } 168 | 169 | if (pkg.scripts.postpublish) { 170 | pkg.scripts['postpublish:old'] = pkg.scripts.postpublish 171 | } 172 | 173 | pkg.scripts.prepublishOnly = 'pinst --disable' 174 | pkg.scripts.postpublish = 'pinst --enable' 175 | pkg.devDependencies.pinst = '^2.1.6' 176 | } 177 | 178 | await fs.promises.writeFile(pkgPath, `${JSON.stringify(pkg, null, originalPkgIndent)}\n`) 179 | 180 | log('Successfully updated package.json') 181 | 182 | process.chdir(options.workingDirectory) 183 | husky.install(huskyDir) 184 | husky.set(path.join(huskyDir, 'pre-commit'), 'npm test') 185 | } 186 | } 187 | 188 | HuskyInstallCommand.description = `Installing husky made easy as woof! 189 | This tool allows you to automatically install husky in several project topologies.` 190 | 191 | HuskyInstallCommand.flags = { 192 | version: oFlags.version({char: 'v'}), 193 | help: oFlags.help({char: 'h'}), 194 | interactive: oFlags.boolean({ 195 | char: 'i', 196 | description: 'turn on interactive mode', 197 | default: false, 198 | }), 199 | destination: oFlags.string({ 200 | char: 'd', 201 | description: "husky's installation directory if different than working directory", 202 | }), 203 | yarn2: oFlags.boolean({ 204 | description: 'setup for yarn 2', 205 | allowNo: true, 206 | }), 207 | pinst: oFlags.boolean({ 208 | char: 'p', 209 | description: 'install and enable pinst', 210 | allowNo: true, 211 | }), 212 | 'fix-gitkraken': oFlags.boolean({ 213 | char: 'g', 214 | description: 'automatically fix Gitkraken incompatibility with husky v5+ (see https://github.com/typicode/husky/issues/875)', 215 | allowNo: true, 216 | }), 217 | } 218 | 219 | HuskyInstallCommand.args = [{ 220 | name: 'workingDirectory', 221 | description: 'Directory where command is executed', 222 | default: CURRENT_WORKING_DIRECTORY, 223 | parse: input => path.resolve(input), 224 | }] 225 | 226 | module.exports = HuskyInstallCommand 227 | --------------------------------------------------------------------------------