@howells/husky 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/bin/howells-husky.mjs +120 -0
- package/hooks/pre-commit +1 -0
- package/hooks/pre-push +2 -0
- package/package.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @howells/husky
|
|
2
|
+
|
|
3
|
+
Standardised git hooks for Howells projects. Immutable pre-commit and pre-push configuration.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -D @howells/husky
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
In `package.json`:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"scripts": {
|
|
18
|
+
"prepare": "howells-husky"
|
|
19
|
+
},
|
|
20
|
+
"lint-staged": {
|
|
21
|
+
"*.{js,ts,jsx,tsx,json,jsonc,css}": "howells-format"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's it. On `pnpm install`, the hooks are installed automatically.
|
|
27
|
+
|
|
28
|
+
## What it does
|
|
29
|
+
|
|
30
|
+
### Pre-commit
|
|
31
|
+
|
|
32
|
+
Runs `pnpm lint-staged` — formats staged files with `howells-format`.
|
|
33
|
+
|
|
34
|
+
### Pre-push
|
|
35
|
+
|
|
36
|
+
Runs `pnpm typecheck` and `pnpm lint`. Both must pass before code reaches the remote.
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
Your `package.json` must have:
|
|
41
|
+
|
|
42
|
+
- `"typecheck"` script (e.g. `tsc --noEmit` or `turbo run typecheck`)
|
|
43
|
+
- `"lint"` script (e.g. `howells-lint` or `turbo run lint`)
|
|
44
|
+
- `"lint-staged"` config using `howells-format`
|
|
45
|
+
|
|
46
|
+
## Why a package?
|
|
47
|
+
|
|
48
|
+
The hooks are shipped from the package, not stored in the project. This means:
|
|
49
|
+
|
|
50
|
+
- Agents can't weaken hooks to get code to pass
|
|
51
|
+
- All projects share identical gate configuration
|
|
52
|
+
- Upgrading the package upgrades the hooks everywhere
|
|
53
|
+
|
|
54
|
+
## Skipped environments
|
|
55
|
+
|
|
56
|
+
Hooks are skipped when `CI=true` or `VERCEL=1` (no git hooks needed in CI/deployment).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @howells/husky — standardised git hooks installer.
|
|
5
|
+
*
|
|
6
|
+
* Usage: add `"prepare": "howells-husky"` to package.json.
|
|
7
|
+
*
|
|
8
|
+
* On `pnpm install`, this script:
|
|
9
|
+
* 1. Runs `husky` to initialise the .husky/ directory
|
|
10
|
+
* 2. Copies the canonical pre-commit and pre-push hooks
|
|
11
|
+
* 3. Validates that lint-staged config exists in package.json
|
|
12
|
+
*
|
|
13
|
+
* The hook scripts are immutable — they come from the package,
|
|
14
|
+
* not from the project. This prevents agents or developers from
|
|
15
|
+
* weakening the configuration to get things to pass.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, chmodSync } from "node:fs";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const packageRoot = resolve(__dirname, "..");
|
|
25
|
+
const projectRoot = process.cwd();
|
|
26
|
+
|
|
27
|
+
// Skip in CI environments — hooks aren't needed there
|
|
28
|
+
if (process.env.CI === "true" || process.env.VERCEL === "1") {
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Skip if not in a git repo (e.g. during npm pack)
|
|
33
|
+
if (!existsSync(join(projectRoot, ".git"))) {
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Step 1: Run husky to initialise .husky/ directory
|
|
38
|
+
const huskyBin = resolve(packageRoot, "node_modules", ".bin", "husky");
|
|
39
|
+
const huskyResult = spawnSync(huskyBin, [], {
|
|
40
|
+
cwd: projectRoot,
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
env: process.env,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (huskyResult.error) {
|
|
46
|
+
// Fallback: try resolving husky from the project's node_modules
|
|
47
|
+
const fallbackResult = spawnSync("npx", ["husky"], {
|
|
48
|
+
cwd: projectRoot,
|
|
49
|
+
stdio: "inherit",
|
|
50
|
+
env: process.env,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (fallbackResult.error || (fallbackResult.status !== null && fallbackResult.status !== 0)) {
|
|
54
|
+
console.error("[@howells/husky] Failed to initialise husky");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Step 2: Copy canonical hook scripts
|
|
60
|
+
const huskyDir = join(projectRoot, ".husky");
|
|
61
|
+
if (!existsSync(huskyDir)) {
|
|
62
|
+
mkdirSync(huskyDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const hooks = ["pre-commit", "pre-push"];
|
|
66
|
+
const hooksDir = join(packageRoot, "hooks");
|
|
67
|
+
|
|
68
|
+
for (const hook of hooks) {
|
|
69
|
+
const source = join(hooksDir, hook);
|
|
70
|
+
const dest = join(huskyDir, hook);
|
|
71
|
+
copyFileSync(source, dest);
|
|
72
|
+
chmodSync(dest, 0o755);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Step 3: Validate lint-staged config
|
|
76
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
77
|
+
if (existsSync(packageJsonPath)) {
|
|
78
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
79
|
+
|
|
80
|
+
if (!packageJson["lint-staged"]) {
|
|
81
|
+
console.warn(
|
|
82
|
+
'[@howells/husky] Warning: no "lint-staged" config found in package.json.',
|
|
83
|
+
);
|
|
84
|
+
console.warn(
|
|
85
|
+
' Add: "lint-staged": { "*.{js,ts,jsx,tsx,json,jsonc,css}": "howells-format" }',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check that the lint-staged command uses howells-format
|
|
90
|
+
const lsConfig = packageJson["lint-staged"];
|
|
91
|
+
if (lsConfig) {
|
|
92
|
+
const commands = Object.values(lsConfig);
|
|
93
|
+
const usesHowells = commands.some(
|
|
94
|
+
(cmd) => typeof cmd === "string" && cmd.includes("howells-format"),
|
|
95
|
+
);
|
|
96
|
+
if (!usesHowells) {
|
|
97
|
+
console.warn(
|
|
98
|
+
"[@howells/husky] Warning: lint-staged should use howells-format.",
|
|
99
|
+
);
|
|
100
|
+
console.warn(
|
|
101
|
+
' Expected: "*.{js,ts,jsx,tsx,json,jsonc,css}": "howells-format"',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check that typecheck and lint scripts exist
|
|
107
|
+
const scripts = packageJson.scripts || {};
|
|
108
|
+
if (!scripts.typecheck) {
|
|
109
|
+
console.warn(
|
|
110
|
+
'[@howells/husky] Warning: no "typecheck" script found. Pre-push hook requires it.',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (!scripts.lint) {
|
|
114
|
+
console.warn(
|
|
115
|
+
'[@howells/husky] Warning: no "lint" script found. Pre-push hook requires it.',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log("[@howells/husky] Hooks installed.");
|
package/hooks/pre-commit
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pnpm lint-staged
|
package/hooks/pre-push
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@howells/husky",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Standardised git hooks for Howells projects. Immutable pre-commit and pre-push configuration.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin/*.mjs",
|
|
8
|
+
"hooks/*",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"bin": {
|
|
12
|
+
"howells-husky": "bin/howells-husky.mjs"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"husky": "^9.1.7",
|
|
16
|
+
"lint-staged": "^16.4.0"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
}
|
|
21
|
+
}
|