lechnerio-git-hooks 1.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.
@@ -0,0 +1,29 @@
1
+ module.exports = {
2
+ extends: ["@commitlint/config-conventional"],
3
+ rules: {
4
+ "type-enum": [
5
+ 2,
6
+ "always",
7
+ [
8
+ "feat",
9
+ "fix",
10
+ "docs",
11
+ "style",
12
+ "refactor",
13
+ "test",
14
+ "chore",
15
+ "perf",
16
+ "revert",
17
+ "ci",
18
+ "typo",
19
+ "fun",
20
+ "update",
21
+ ],
22
+ ],
23
+ "type-case": [2, "always", "lower-case"],
24
+ "type-empty": [2, "never"],
25
+ "subject-empty": [2, "never"],
26
+ "subject-full-stop": [2, "never", "."],
27
+ "subject-case": [2, "always", "lower-case"],
28
+ },
29
+ }
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
3
+ "*.{json,md,css}": ["prettier --write"],
4
+ }
@@ -0,0 +1 @@
1
+ npx --no-install commitlint --edit
@@ -0,0 +1,292 @@
1
+ #!/bin/bash
2
+
3
+ RED='\033[0;31m'
4
+ GREEN='\033[0;32m'
5
+ YELLOW='\033[1;33m'
6
+ BLUE='\033[0;34m'
7
+ CYAN='\033[0;36m'
8
+ GRAY='\033[0;90m'
9
+ NC='\033[0m'
10
+
11
+ if [ "$_SKIP_POST_COMMIT" = "1" ]; then
12
+ exit 0
13
+ fi
14
+ export _SKIP_POST_COMMIT=1
15
+
16
+ DRY_RUN=${1:-false}
17
+
18
+ get_version() {
19
+ node -p "require('./package.json').version"
20
+ }
21
+
22
+ get_subpatch() {
23
+ local ver=$(get_version)
24
+ if [[ "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-z])$ ]]; then
25
+ echo "${BASH_REMATCH[1]}"
26
+ else
27
+ echo ""
28
+ fi
29
+ }
30
+
31
+ get_base_version() {
32
+ local ver=$(get_version)
33
+ echo "$ver" | sed 's/[a-z]$//'
34
+ }
35
+
36
+ next_subpatch() {
37
+ local current_sub=$(get_subpatch)
38
+ if [ -z "$current_sub" ]; then
39
+ echo "a"
40
+ else
41
+ echo "$current_sub" | tr 'a-y' 'b-z'
42
+ fi
43
+ }
44
+
45
+ set_version() {
46
+ local new_ver="$1"
47
+ node -e "
48
+ const fs = require('fs');
49
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
50
+ pkg.version = '$new_ver';
51
+ fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
52
+ "
53
+ }
54
+
55
+ bump_and_amend() {
56
+ local new_ver="$1"
57
+ local label="$2"
58
+ set_version "$new_ver"
59
+ git add package.json
60
+ git commit --amend --no-edit
61
+ git tag -f -a "v${new_ver}" -m "v${new_ver}"
62
+ node scripts/generate-changelog.mjs 2>/dev/null
63
+ git add data/changelog.json
64
+ git commit --amend --no-edit --no-verify
65
+ git tag -f -a "v${new_ver}" -m "v${new_ver}"
66
+ echo -e "${GREEN}✅ $label version bumped to ${new_ver}${NC}"
67
+ }
68
+
69
+ commit_message=$(git log -1 --pretty=%B)
70
+ if [[ "$commit_message" == *"Bump version"* ]] || [[ "$commit_message" == *"bump version"* ]]; then
71
+ echo -e "${YELLOW}⏭️ Skipping post-commit actions for version bump commit${NC}"
72
+ exit 0
73
+ fi
74
+
75
+ echo
76
+ echo -e "${GREEN}🎉 Commit completed successfully!${NC}"
77
+ echo
78
+
79
+ node scripts/generate-changelog.mjs 2>/dev/null
80
+ if ! git diff --quiet data/changelog.json 2>/dev/null; then
81
+ git add data/changelog.json
82
+ git commit --amend --no-edit --no-verify
83
+ echo -e "${CYAN}📋 Changelog updated${NC}"
84
+ echo
85
+ fi
86
+
87
+ files_changed=$(git diff --name-only HEAD~1 HEAD 2>/dev/null | wc -l)
88
+ lines_changed=$(git diff --shortstat HEAD~1 HEAD 2>/dev/null)
89
+
90
+ if [ "$files_changed" -eq 0 ] || [ -z "$lines_changed" ]; then
91
+ echo -e "${YELLOW}⚠️ No file changes detected in this commit.${NC}"
92
+ echo -e "${YELLOW}⏭️ Skipping version bump.${NC}"
93
+ echo
94
+
95
+ if [ -n "$POST_ACTION" ]; then
96
+ choice="$POST_ACTION"
97
+ elif exec < /dev/tty 2>/dev/null; then
98
+ echo -e "${CYAN}What would you like to do next?${NC}"
99
+ echo
100
+ echo "1) Push to GitHub"
101
+ echo "2) Merge to Main"
102
+ echo -e "${GRAY}0) None${NC}"
103
+ echo
104
+ echo -n "Select option (1-2, Enter for Default or 0 to skip): "
105
+ read -r choice
106
+ choice=${choice:-1}
107
+ else
108
+ choice=0
109
+ fi
110
+ echo
111
+
112
+ case $choice in
113
+ 1)
114
+ echo -e "${YELLOW}✈️ Pushing to GitHub...${NC}"
115
+ if [ "$DRY_RUN" = "true" ]; then
116
+ echo -e "${YELLOW}[DRY RUN] Would execute: git push${NC}"
117
+ else
118
+ git push --follow-tags && git push --tags
119
+ if [ $? -eq 0 ]; then
120
+ echo -e "${GREEN}✅ Successfully pushed to GitHub${NC}"
121
+ else
122
+ echo -e "${RED}❌ Failed to push to GitHub${NC}"
123
+ fi
124
+ fi
125
+ ;;
126
+ 2)
127
+ echo -e "${YELLOW}🔄 Merging to main...${NC}"
128
+ if [ "$DRY_RUN" = "true" ]; then
129
+ echo -e "${YELLOW}[DRY RUN] Would execute: git checkout main && git merge $(git branch --show-current)${NC}"
130
+ else
131
+ current_branch=$(git branch --show-current)
132
+ git checkout main
133
+ git merge "$current_branch"
134
+ if [ $? -eq 0 ]; then
135
+ echo -e "${GREEN}✅ Successfully merged to main${NC}"
136
+ git checkout "$current_branch"
137
+ else
138
+ echo -e "${RED}❌ Failed to merge to main${NC}"
139
+ git checkout "$current_branch"
140
+ fi
141
+ fi
142
+ ;;
143
+ 0)
144
+ echo -e "${YELLOW}⏭️ Skipping additional actions${NC}"
145
+ ;;
146
+ esac
147
+ echo
148
+ exit 0
149
+ fi
150
+
151
+ echo -e "${BLUE}📝 Changes in this commit:${NC}"
152
+ echo -e "${YELLOW}$lines_changed${NC}"
153
+ echo
154
+
155
+ current_ver=$(get_version)
156
+ base_ver=$(get_base_version)
157
+ sub=$(get_subpatch)
158
+ next_sub=$(next_subpatch)
159
+
160
+ IFS='.' read -r major minor patch_raw <<< "$base_ver"
161
+ patch=$((patch_raw))
162
+ next_patch=$((patch + 1))
163
+ next_minor=$((minor + 1))
164
+ next_major=$((major + 1))
165
+
166
+ subtle_ver="${base_ver}${next_sub}"
167
+ patch_ver="${major}.${minor}.${next_patch}"
168
+ minor_ver="${major}.${next_minor}.0"
169
+ major_ver="${next_major}.0.0"
170
+
171
+ if [ -n "$VERSION_BUMP" ]; then
172
+ version_choice="$VERSION_BUMP"
173
+ elif exec < /dev/tty 2>/dev/null; then
174
+ echo -e "${CYAN}What version bump would you like to apply?${NC}"
175
+ echo
176
+ echo -e "1) Subtle Bump ${YELLOW}(default)${NC} ${GRAY}${current_ver} -> ${subtle_ver}${NC} - typos, formatting, comments, etc."
177
+ echo -e "2) Bump Patch ${GRAY}${current_ver} -> ${patch_ver}${NC} - bug fixes, small changes"
178
+ echo -e "3) Bump Minor ${GRAY}${current_ver} -> ${minor_ver}${NC} - new features, backwards compatible"
179
+ echo -e "4) Bump Major ${GRAY}${current_ver} -> ${major_ver}${NC} - breaking changes"
180
+ echo -e "${GRAY}0) Nothing - skip version bump${NC}"
181
+ echo
182
+ echo -n "Select option (1-4, Enter for Default or 0 to skip): "
183
+ read -r version_choice
184
+ version_choice=${version_choice:-1}
185
+ else
186
+ echo -e "${YELLOW}⏭️ No TTY available and VERSION_BUMP not set, skipping version bump${NC}"
187
+ version_choice=0
188
+ fi
189
+
190
+ echo
191
+
192
+ case $version_choice in
193
+ 1)
194
+ echo -e "${YELLOW}📦 Subtle bump...${NC}"
195
+ if [ "$DRY_RUN" = "true" ]; then
196
+ echo -e "${YELLOW}[DRY RUN] Would bump to ${subtle_ver}${NC}"
197
+ else
198
+ bump_and_amend "$subtle_ver" "Subtle"
199
+ fi
200
+ ;;
201
+ 2)
202
+ echo -e "${YELLOW}📦 Bumping patch version...${NC}"
203
+ if [ "$DRY_RUN" = "true" ]; then
204
+ echo -e "${YELLOW}[DRY RUN] Would bump to ${patch_ver}${NC}"
205
+ else
206
+ bump_and_amend "$patch_ver" "Patch"
207
+ fi
208
+ ;;
209
+ 3)
210
+ echo -e "${YELLOW}📦 Bumping minor version...${NC}"
211
+ if [ "$DRY_RUN" = "true" ]; then
212
+ echo -e "${YELLOW}[DRY RUN] Would bump to ${minor_ver}${NC}"
213
+ else
214
+ bump_and_amend "$minor_ver" "Minor"
215
+ fi
216
+ ;;
217
+ 4)
218
+ echo -e "${YELLOW}📦 Bumping major version...${NC}"
219
+ if [ "$DRY_RUN" = "true" ]; then
220
+ echo -e "${YELLOW}[DRY RUN] Would bump to ${major_ver}${NC}"
221
+ else
222
+ bump_and_amend "$major_ver" "Major"
223
+ fi
224
+ ;;
225
+ 0)
226
+ echo -e "${YELLOW}⏭️ Skipping version bump${NC}"
227
+ ;;
228
+ *)
229
+ echo -e "${RED}❌ Invalid option. Skipping version bump${NC}"
230
+ ;;
231
+ esac
232
+
233
+ echo
234
+
235
+ if [ -n "$POST_ACTION" ]; then
236
+ choice="$POST_ACTION"
237
+ elif exec < /dev/tty 2>/dev/null; then
238
+ echo -e "${CYAN}What would you like to do next?${NC}"
239
+ echo
240
+ echo -e "1) Push to GitHub ${YELLOW}(default)${NC}"
241
+ echo "2) Merge to Main"
242
+ echo -e "${GRAY}0) None${NC}"
243
+ echo
244
+ echo -n "Select option (1-2, Enter for Default or 0 to skip): "
245
+ read -r choice
246
+ choice=${choice:-1}
247
+ else
248
+ choice=0
249
+ fi
250
+
251
+ echo
252
+
253
+ case $choice in
254
+ 1)
255
+ echo -e "${YELLOW}✈️ Pushing to GitHub...${NC}"
256
+ if [ "$DRY_RUN" = "true" ]; then
257
+ echo -e "${YELLOW}[DRY RUN] Would execute: git push --follow-tags && git push --tags${NC}"
258
+ else
259
+ git push --follow-tags && git push --tags
260
+ if [ $? -eq 0 ]; then
261
+ echo -e "${GREEN}✅ Successfully pushed to GitHub${NC}"
262
+ else
263
+ echo -e "${RED}❌ Failed to push to GitHub${NC}"
264
+ fi
265
+ fi
266
+ ;;
267
+ 2)
268
+ echo -e "${YELLOW}🔄 Merging to main...${NC}"
269
+ if [ "$DRY_RUN" = "true" ]; then
270
+ echo -e "${YELLOW}[DRY RUN] Would execute: git checkout main && git merge $(git branch --show-current)${NC}"
271
+ else
272
+ current_branch=$(git branch --show-current)
273
+ git checkout main
274
+ git merge "$current_branch"
275
+ if [ $? -eq 0 ]; then
276
+ echo -e "${GREEN}✅ Successfully merged to main${NC}"
277
+ git checkout "$current_branch"
278
+ else
279
+ echo -e "${RED}❌ Failed to merge to main${NC}"
280
+ git checkout "$current_branch"
281
+ fi
282
+ fi
283
+ ;;
284
+ 0)
285
+ echo -e "${YELLOW}⏭️ Skipping additional actions${NC}"
286
+ ;;
287
+ *)
288
+ echo -e "${RED}❌ Invalid option. Skipping additional actions${NC}"
289
+ ;;
290
+ esac
291
+
292
+ echo
@@ -0,0 +1,4 @@
1
+ echo "Running lint-staged..."
2
+ pnpm run lint-staged || exit 1
3
+
4
+ echo "🎉 pre-commit hook completed."
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "lechnerio-git-hooks",
3
+ "version": "1.1.0",
4
+ "description": "Shared git hooks for lechnerio projects",
5
+ "type": "module",
6
+ "scripts": {
7
+ "postinstall": "node setup.mjs"
8
+ },
9
+ "files": [
10
+ "hooks",
11
+ "scripts",
12
+ "config",
13
+ "setup.mjs"
14
+ ],
15
+ "dependencies": {
16
+ "@commitlint/cli": "^20.4.1",
17
+ "@commitlint/config-conventional": "^20.4.1",
18
+ "husky": "^9.1.7",
19
+ "lint-staged": "^16.2.7"
20
+ }
21
+ }
@@ -0,0 +1,77 @@
1
+ import { execSync } from "child_process"
2
+ import { writeFileSync, mkdirSync } from "fs"
3
+
4
+ const run = (cmd) => execSync(cmd, { encoding: "utf-8", shell: "bash" }).trim()
5
+
6
+ try {
7
+ run("git --version")
8
+ run("git rev-parse --git-dir")
9
+ } catch {
10
+ console.warn("git not available, skipping changelog generation")
11
+ process.exit(0)
12
+ }
13
+
14
+ const tags = run("git tag -l --sort=-version:refname").split("\n").filter(Boolean)
15
+ if (tags.length === 0) {
16
+ console.warn("no git tags found, keeping existing changelog")
17
+ process.exit(0)
18
+ }
19
+
20
+ const changelog = []
21
+
22
+ const currentVersion = tags[0].replace(/^v/, "")
23
+ const unreleasedRaw = run(`git log --format="%s" ${tags[0]}..HEAD`)
24
+ if (unreleasedRaw) {
25
+ const commits = [...new Set(unreleasedRaw.split("\n").filter(Boolean))].map((message) => ({ message }))
26
+ if (commits.length > 0) {
27
+ const date = run(`git log --format=%ai HEAD -1`).split(" ")[0]
28
+ changelog.push({ version: currentVersion, tag: null, date, commits })
29
+ }
30
+ }
31
+
32
+ for (let i = 0; i < tags.length; i++) {
33
+ const tag = tags[i]
34
+ const date = run(`git log --format=%ai ${tag} -1`).split(" ")[0]
35
+ const prevTag = tags[i + 1]
36
+
37
+ const range = prevTag ? `${prevTag}..${tag}` : tag
38
+ const raw = run(`git log --format="%s" ${range}`)
39
+
40
+ const commits = raw
41
+ ? [...new Set(raw.split("\n").filter(Boolean))].map((message) => ({ message }))
42
+ : []
43
+
44
+ changelog.push({
45
+ version: tag.replace(/^v/, ""),
46
+ tag,
47
+ date,
48
+ commits,
49
+ })
50
+ }
51
+
52
+ const baseVersion = (v) => v.replace(/[a-z]+$/i, "")
53
+
54
+ const grouped = []
55
+ for (const entry of changelog) {
56
+ const base = baseVersion(entry.version)
57
+ const existing = grouped.find((g) => g.version === base && g.tag !== null)
58
+ if (existing) {
59
+ existing.commits.push(...entry.commits)
60
+ if (entry.date < existing.date) existing.date = entry.date
61
+ } else {
62
+ grouped.push({ ...entry, version: base })
63
+ }
64
+ }
65
+
66
+ for (const entry of grouped) {
67
+ const seen = new Set()
68
+ entry.commits = entry.commits.filter((c) => {
69
+ if (seen.has(c.message)) return false
70
+ seen.add(c.message)
71
+ return true
72
+ })
73
+ }
74
+
75
+ mkdirSync("data", { recursive: true })
76
+ writeFileSync("data/changelog.json", JSON.stringify(grouped, null, 2))
77
+ console.log(`changelog generated: ${grouped.length} versions`)
package/setup.mjs ADDED
@@ -0,0 +1,70 @@
1
+ import { cpSync, mkdirSync, existsSync, readFileSync, writeFileSync } from "fs"
2
+ import { resolve, dirname } from "path"
3
+ import { fileURLToPath } from "url"
4
+ import { execSync } from "child_process"
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+
8
+ function findProjectRoot(startDir) {
9
+ let dir = startDir
10
+ while (true) {
11
+ if (!dir.includes("node_modules") && existsSync(resolve(dir, "package.json"))) {
12
+ return dir
13
+ }
14
+ const parent = dirname(dir)
15
+ if (parent === dir) return null
16
+ dir = parent
17
+ }
18
+ }
19
+
20
+ const projectRoot = findProjectRoot(__dirname)
21
+
22
+ if (!projectRoot) {
23
+ process.exit(0)
24
+ }
25
+
26
+ const pkgPath = resolve(projectRoot, "package.json")
27
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
28
+
29
+ const hooksSource = resolve(__dirname, "hooks")
30
+ const scriptsSource = resolve(__dirname, "scripts")
31
+ const configSource = resolve(__dirname, "config")
32
+
33
+ const huskyDest = resolve(projectRoot, ".husky")
34
+ const scriptsDest = resolve(projectRoot, "scripts")
35
+
36
+ mkdirSync(huskyDest, { recursive: true })
37
+ mkdirSync(scriptsDest, { recursive: true })
38
+
39
+ cpSync(resolve(hooksSource, "post-commit"), resolve(huskyDest, "post-commit"))
40
+ cpSync(resolve(hooksSource, "pre-commit"), resolve(huskyDest, "pre-commit"))
41
+ cpSync(resolve(hooksSource, "commit-msg"), resolve(huskyDest, "commit-msg"))
42
+ cpSync(resolve(scriptsSource, "generate-changelog.mjs"), resolve(scriptsDest, "generate-changelog.mjs"))
43
+ cpSync(resolve(configSource, "commitlint.config.js"), resolve(projectRoot, "commitlint.config.js"))
44
+ cpSync(resolve(configSource, "lintstagedrc.js"), resolve(projectRoot, ".lintstagedrc.js"))
45
+
46
+ let modified = false
47
+
48
+ if (!pkg.scripts) pkg.scripts = {}
49
+
50
+ if (!pkg.scripts.prepare || !pkg.scripts.prepare.includes("husky")) {
51
+ pkg.scripts.prepare = "husky"
52
+ modified = true
53
+ }
54
+
55
+ if (!pkg.scripts["lint-staged"]) {
56
+ pkg.scripts["lint-staged"] = "lint-staged"
57
+ modified = true
58
+ }
59
+
60
+ if (modified) {
61
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n")
62
+ console.log("lechnerio-git-hooks: updated package.json scripts")
63
+ }
64
+
65
+ try {
66
+ execSync("git config core.hooksPath .husky", { cwd: projectRoot, stdio: "ignore" })
67
+ console.log("lechnerio-git-hooks: git hooks path configured")
68
+ } catch {}
69
+
70
+ console.log("lechnerio-git-hooks: setup complete")