Skip to content

Commit 8cd50a7

Browse files
committed
feat(hooks): make hook tooling config-driven
- Remove hardcoded auto-run defaults from TOOLS array; it is now a pure definition registry used only for name lookups - Add .husky/.git-hooks.config.dist.json as the canonical defaults template, pre-configured with all built-in tools under hooks.preCommit.tools and an empty hooks.prePush.tools - Add .husky/generate-config.ts (ZX script) to copy the dist template to .git-hooks.config.json; callable via npm run hooks:init - Add hooks:init script to package.json - loadConfig() now returns null when no config file is found; each hook and runHook() exit 0 with a clear message pointing to hooks:init instead of silently running all tools - applyConfigOverrides() simplified: purely config-driven for all hooks, no more preCommit/other-hook split logic - isHookSkippedByConfig() and applyVerboseSetting() accept null config - Fix .husky glob in files.sh to use find -maxdepth 1 so dotfiles (like .git-hooks.config.dist.json) are copied correctly during integration - Add generate_hooks_config() to integration script, called after pnpm install, to auto-generate .git-hooks.config.json on fresh installs (idempotent) - Update integration test expected files list - Fix stale extras.test.ts: HTML generation step was removed from generateApiDocs(); update test to reflect current two-step behaviour - All 145 unit tests pass
1 parent dffcab4 commit 8cd50a7

20 files changed

+764
-273
lines changed

booster/.git-hooks.config.example.json

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,61 @@
2323
"artifacts": false
2424
},
2525

26-
"tools": {
27-
"// Disable a tool": {
28-
"enabled": false
29-
},
26+
"hooks": {
27+
"preCommit": {
28+
"// Note": "All default tools run; overrides here are merged on top.",
29+
"tools": {
30+
"// Disable a tool": {
31+
"enabled": false
32+
},
3033

31-
"// Override arguments": {
32-
"args": ["analyse", "--level=max"]
33-
},
34+
"// Override arguments": {
35+
"args": ["analyse", "--level=max"]
36+
},
3437

35-
"// Override extensions": {
36-
"extensions": [".php", ".phtml"]
37-
},
38+
"// Override extensions": {
39+
"extensions": [".php", ".phtml"]
40+
},
3841

39-
"// Change failure mode": {
40-
"onFailure": "stop"
41-
},
42+
"// Change failure mode": {
43+
"onFailure": "stop"
44+
},
4245

43-
"// Example: Disable Psalm": {
44-
"// Psalm": { "enabled": false }
45-
},
46+
"// Example: Disable Psalm": {
47+
"// Psalm": { "enabled": false }
48+
},
49+
50+
"// Example: Increase PHPStan level": {
51+
"// PHPStan": { "args": ["analyse", "--level=9"] }
52+
},
4653

47-
"// Example: Increase PHPStan level": {
48-
"// PHPStan": { "args": ["analyse", "--level=9"] }
54+
"// Example: Add custom tool": {
55+
"// CustomLinter": {
56+
"command": "vendor/bin/custom-lint",
57+
"args": ["--strict"],
58+
"type": "php",
59+
"extensions": [".php"],
60+
"description": "Custom PHP linter"
61+
}
62+
}
63+
}
4964
},
5065

51-
"// Example: Add custom tool": {
52-
"// CustomLinter": {
53-
"command": "vendor/bin/custom-lint",
54-
"args": ["--strict"],
55-
"type": "php",
56-
"extensions": [".php"],
57-
"description": "Custom PHP linter"
66+
"prePush": {
67+
"// Note": "ONLY the tools listed here run during pre-push.",
68+
"// Note2": "Use passFiles:false for whole-project analysis (PHPStan, Psalm, Deptrac, etc.).",
69+
"tools": {
70+
"// Example: Run full PHPStan on push (no file-by-file filtering)": {
71+
"// PHPStan": { "passFiles": false }
72+
},
73+
74+
"// Example: Run a custom tool only on push": {
75+
"// MyAnalyser": {
76+
"command": "vendor/bin/my-analyser",
77+
"type": "php",
78+
"passFiles": false
79+
}
80+
}
5881
}
5982
}
6083
}

booster/.git-hooks.config.schema.json

Lines changed: 59 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -42,73 +42,57 @@
4242
},
4343
"additionalProperties": false
4444
},
45-
"tools": {
45+
"hooks": {
4646
"type": "object",
47-
"description": "Tool-specific configuration overrides. Tool names are case-insensitive.",
47+
"description": "Per-hook configuration. Replaces the old global 'tools' key — configure tools for each hook independently.",
4848
"properties": {
49-
"ESLint": {
50-
"$ref": "#/definitions/toolOverride",
51-
"description": "ESLint - JavaScript/TypeScript linting (group: lint)"
52-
},
53-
"Prettier": {
54-
"$ref": "#/definitions/toolOverride",
55-
"description": "Prettier - Code formatting (group: format)"
56-
},
57-
"Stylelint": {
58-
"$ref": "#/definitions/toolOverride",
59-
"description": "Stylelint - CSS/SCSS linting (group: lint)"
60-
},
61-
"TypeScript": {
62-
"$ref": "#/definitions/toolOverride",
63-
"description": "TypeScript - Type checking (group: lint)"
64-
},
65-
"Markdownlint": {
66-
"$ref": "#/definitions/toolOverride",
67-
"description": "Markdownlint - Markdown style checker (group: lint)"
68-
},
69-
"Flatten JSON": {
70-
"$ref": "#/definitions/toolOverride",
71-
"description": "Flatten JSON - Normalizes JSON files (group: format)"
72-
},
73-
"PHP Syntax Check": {
74-
"$ref": "#/definitions/toolOverride",
75-
"description": "PHP Syntax Check - PHP lint (group: lint)"
76-
},
77-
"Rector": {
78-
"$ref": "#/definitions/toolOverride",
79-
"description": "Rector - PHP code refactoring (group: refactor)"
80-
},
81-
"ECS": {
82-
"$ref": "#/definitions/toolOverride",
83-
"description": "ECS - Easy Coding Standard formatter (group: format)"
84-
},
85-
"PHPStan": {
86-
"$ref": "#/definitions/toolOverride",
87-
"description": "PHPStan - PHP static analysis (group: analysis)"
88-
},
89-
"Psalm": {
90-
"$ref": "#/definitions/toolOverride",
91-
"description": "Psalm - PHP static analysis (group: analysis)"
92-
},
93-
"Deptrac": {
94-
"$ref": "#/definitions/toolOverride",
95-
"description": "Deptrac - Architecture layer dependency checker (group: analysis)"
49+
"preCommit": {
50+
"$ref": "#/definitions/hookConfig",
51+
"description": "Pre-commit hook configuration. All default tools run; overrides are applied on top."
9652
},
97-
"PHPUnit": {
98-
"$ref": "#/definitions/toolOverride",
99-
"description": "PHPUnit - PHP unit testing"
53+
"prePush": {
54+
"$ref": "#/definitions/hookConfig",
55+
"description": "Pre-push hook configuration. ONLY the tools listed here will run as quality checks. Use passFiles:false for whole-project analysis tools."
10056
},
101-
"API Docs": {
102-
"$ref": "#/definitions/toolOverride",
103-
"description": "API Docs - OpenAPI documentation generation"
57+
"commitMsg": {
58+
"$ref": "#/definitions/hookConfig",
59+
"description": "Commit-msg hook configuration. ONLY the tools listed here will run."
10460
}
10561
},
106-
"additionalProperties": {
107-
"$ref": "#/definitions/customTool"
108-
}
62+
"additionalProperties": false
10963
}
11064
},
11165
"definitions": {
66+
"hookConfig": {
67+
"type": "object",
68+
"description": "Per-hook configuration",
69+
"properties": {
70+
"tools": {
71+
"type": "object",
72+
"description": "Tool overrides/additions scoped to this hook. Tool names are case-insensitive. For pre-commit: all default tools run and these are merged on top. For pre-push/commit-msg: ONLY tools listed here will run.",
73+
"properties": {
74+
"ESLint": { "$ref": "#/definitions/toolOverride", "description": "ESLint - JavaScript/TypeScript linting (group: lint)" },
75+
"Prettier": { "$ref": "#/definitions/toolOverride", "description": "Prettier - Code formatting (group: format)" },
76+
"Stylelint": { "$ref": "#/definitions/toolOverride", "description": "Stylelint - CSS/SCSS linting (group: lint)" },
77+
"TypeScript": { "$ref": "#/definitions/toolOverride", "description": "TypeScript - Type checking (group: lint)" },
78+
"Markdownlint": { "$ref": "#/definitions/toolOverride", "description": "Markdownlint - Markdown style checker (group: lint)" },
79+
"Flatten JSON": { "$ref": "#/definitions/toolOverride", "description": "Flatten JSON - Normalizes JSON files (group: format)" },
80+
"PHP Syntax Check": { "$ref": "#/definitions/toolOverride", "description": "PHP Syntax Check - PHP lint (group: lint)" },
81+
"Rector": { "$ref": "#/definitions/toolOverride", "description": "Rector - PHP code refactoring (group: refactor)" },
82+
"ECS": { "$ref": "#/definitions/toolOverride", "description": "ECS - Easy Coding Standard formatter (group: format)" },
83+
"PHPStan": { "$ref": "#/definitions/toolOverride", "description": "PHPStan - PHP static analysis (group: analysis)" },
84+
"Psalm": { "$ref": "#/definitions/toolOverride", "description": "Psalm - PHP static analysis (group: analysis)" },
85+
"Deptrac": { "$ref": "#/definitions/toolOverride", "description": "Deptrac - Architecture layer dependency checker (group: analysis)" },
86+
"PHPUnit": { "$ref": "#/definitions/toolOverride", "description": "PHPUnit - PHP unit testing" },
87+
"API Docs": { "$ref": "#/definitions/toolOverride", "description": "API Docs - OpenAPI documentation generation" }
88+
},
89+
"additionalProperties": {
90+
"$ref": "#/definitions/customTool"
91+
}
92+
}
93+
},
94+
"additionalProperties": false
95+
},
11296
"toolOverride": {
11397
"type": "object",
11498
"description": "Override existing tool settings to customize behavior or replace functionality",
@@ -250,9 +234,23 @@
250234
"skip": {
251235
"tests": true
252236
},
253-
"tools": {
254-
"Psalm": { "enabled": false },
255-
"PHPStan": { "args": ["analyse", "--level=9"] }
237+
"hooks": {
238+
"preCommit": {
239+
"tools": {
240+
"Psalm": { "enabled": false },
241+
"PHPStan": { "args": ["analyse", "--level=9"] }
242+
}
243+
},
244+
"prePush": {
245+
"tools": {
246+
"PHPStan": { "passFiles": false },
247+
"MyCustomAnalyser": {
248+
"command": "vendor/bin/my-analyser",
249+
"type": "php",
250+
"passFiles": false
251+
}
252+
}
253+
}
256254
}
257255
}
258256
]
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
{
2+
"$schema": ".git-hooks.config.schema.json",
3+
4+
"verbose": false,
5+
6+
"skip": {
7+
"preCommit": false,
8+
"prePush": false,
9+
"commitMsg": false,
10+
"tests": false,
11+
"artifacts": false
12+
},
13+
14+
"hooks": {
15+
"preCommit": {
16+
"tools": {
17+
"ESLint": {
18+
"command": "eslint",
19+
"args": ["--fix", "--cache", "--no-warn-ignored"],
20+
"type": "node",
21+
"stagesFilesAfter": true,
22+
"extensions": [".js", ".jsx", ".ts", ".tsx", ".vue", ".mjs", ".cjs"],
23+
"group": "lint"
24+
},
25+
"Prettier": {
26+
"command": "prettier",
27+
"args": ["--write", "--ignore-unknown", "--cache"],
28+
"type": "node",
29+
"stagesFilesAfter": true,
30+
"extensions": [".js", ".jsx", ".ts", ".tsx", ".vue", ".mjs", ".cjs", ".json", ".md", ".yml", ".yaml", ".css", ".scss"],
31+
"group": "format"
32+
},
33+
"Stylelint": {
34+
"command": "stylelint",
35+
"args": ["--fix", "--allow-empty-input", "--cache"],
36+
"type": "node",
37+
"stagesFilesAfter": true,
38+
"extensions": [".vue", ".css", ".scss", ".sass", ".less"],
39+
"group": "lint"
40+
},
41+
"TypeScript": {
42+
"command": "tsc",
43+
"args": ["--noEmit", "--skipLibCheck"],
44+
"type": "node",
45+
"passFiles": false,
46+
"extensions": [".ts", ".tsx"],
47+
"group": "lint",
48+
"description": "Type-checking TypeScript files..."
49+
},
50+
"Markdownlint": {
51+
"command": "markdownlint-cli2",
52+
"args": [],
53+
"type": "node",
54+
"extensions": [".md"],
55+
"group": "lint"
56+
},
57+
"PHP Syntax Check": {
58+
"command": "php",
59+
"args": ["-l", "-d", "display_errors=0"],
60+
"type": "php",
61+
"runForEachFile": true,
62+
"extensions": [".php"],
63+
"onFailure": "stop",
64+
"group": "lint"
65+
},
66+
"Rector": {
67+
"command": "rector",
68+
"args": ["process"],
69+
"type": "php",
70+
"stagesFilesAfter": true,
71+
"extensions": [".php"],
72+
"group": "refactor"
73+
},
74+
"ECS": {
75+
"command": "ecs",
76+
"args": ["check", "--fix"],
77+
"type": "php",
78+
"stagesFilesAfter": true,
79+
"extensions": [".php"],
80+
"group": "format"
81+
},
82+
"PHPStan": {
83+
"command": "phpstan",
84+
"args": ["analyse"],
85+
"type": "php",
86+
"extensions": [".php"],
87+
"group": "analysis"
88+
},
89+
"Psalm": {
90+
"command": "psalm",
91+
"type": "php",
92+
"extensions": [".php"],
93+
"group": "analysis"
94+
},
95+
"Deptrac": {
96+
"command": "deptrac",
97+
"args": ["analyse", "--no-cache"],
98+
"type": "php",
99+
"passFiles": false,
100+
"extensions": [".php"],
101+
"group": "analysis"
102+
}
103+
}
104+
},
105+
"prePush": {
106+
"tools": {}
107+
}
108+
}
109+
}

booster/.husky/generate-config.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env zx
2+
3+
/**
4+
* Generate .git-hooks.config.json from the dist template.
5+
*
6+
* Usage:
7+
* zx .husky/generate-config.ts # interactive
8+
* npm run hooks:init # same via package.json script
9+
*/
10+
11+
import { chalk, fs } from 'zx'
12+
import { resolve, dirname } from 'path'
13+
import { fileURLToPath } from 'url'
14+
import { createInterface } from 'readline'
15+
16+
const __dirname = dirname(fileURLToPath(import.meta.url))
17+
const DIST_PATH = resolve(__dirname, '.git-hooks.config.dist.json')
18+
const TARGET_PATH = resolve(process.cwd(), '.git-hooks.config.json')
19+
20+
async function prompt(question: string): Promise<string> {
21+
const rl = createInterface({ input: process.stdin, output: process.stdout })
22+
return new Promise((res) => {
23+
rl.question(question, (answer) => {
24+
rl.close()
25+
res(answer.trim())
26+
})
27+
})
28+
}
29+
30+
if (!(await fs.pathExists(DIST_PATH))) {
31+
console.error(chalk.red(`✗ Dist template not found: ${DIST_PATH}`))
32+
process.exit(1)
33+
}
34+
35+
const distContents = await fs.readFile(DIST_PATH, 'utf-8')
36+
37+
if (await fs.pathExists(TARGET_PATH)) {
38+
const answer = await prompt(chalk.yellow('.git-hooks.config.json already exists. Overwrite? [y/N] '))
39+
if (!answer.match(/^y(es)?$/i)) {
40+
console.log('Aborted. Existing config kept.')
41+
process.exit(0)
42+
}
43+
}
44+
45+
await fs.writeFile(TARGET_PATH, distContents, 'utf-8')
46+
47+
console.log(chalk.green('✓ Created .git-hooks.config.json'))
48+
console.log(chalk.gray(' Edit it to enable/disable tools for each hook.'))
49+
console.log(chalk.gray(' Run `npm run hooks:init` again to reset to defaults.'))

0 commit comments

Comments
 (0)