@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 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.");
@@ -0,0 +1 @@
1
+ pnpm lint-staged
package/hooks/pre-push ADDED
@@ -0,0 +1,2 @@
1
+ pnpm typecheck || exit 1
2
+ pnpm lint || exit 1
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
+ }