@archpublicwebsite/eslint-config 1.0.5

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,145 @@
1
+ # eslint-config
2
+
3
+ Reusable ESLint + Git hooks toolkit for Nuxt/Vue projects.
4
+
5
+ ## Publish
6
+
7
+ ```bash
8
+ cd packages/eslint-config
9
+ pnpm version patch
10
+ npm publish --access public
11
+ ```
12
+
13
+ Quick checks before publish:
14
+
15
+ - Ensure `.npmrc` points to public npm registry.
16
+ - Run `npm pack --dry-run` and verify only `eslint.config.mjs`, `tools/`, and `README.md` are included.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pnpm add -Dw eslint-config
22
+ ```
23
+
24
+ ### Required dependencies (consumer project)
25
+
26
+ This toolkit expects these dev dependencies to exist in the project that installs it:
27
+
28
+ ```bash
29
+ pnpm add -D lint-staged prettier turbo
30
+ ```
31
+
32
+ - `lint-staged` is used by the generated `pre-commit` hook.
33
+ - `prettier` is used by the `lint:fix` flow.
34
+ - `turbo` is used by the required `lint:check` / `lint:fix` scripts.
35
+
36
+ On install, this package automatically sets up in your project root:
37
+
38
+ - `.hooks/pre-commit`
39
+ - `.hooks/prepare-commit-msg`
40
+ - `.hooks/commit-msg`
41
+ - `.hooks/post-commit`
42
+ - `eslint.config.mjs` (if not present)
43
+ - `.prettierrc` plugin entry for `prettier-plugin-tailwindcss`
44
+ - `git config core.hooksPath .hooks` (when in a git repo)
45
+
46
+ ## What this package provides
47
+
48
+ - `eslint.config.mjs` builder via `createArchipelagoConfig`
49
+ - Reusable git hook handlers in `tools/git-hooks/`
50
+ - `pre-commit.mjs`
51
+ - `prepare-commit-msg.mjs`
52
+ - `commit-msg.mjs`
53
+ - `post-commit.mjs`
54
+ - `generate-commit-message.mjs`
55
+ - `verify-commit-message.mjs`
56
+ - `tools/setup/install.mjs` automatic project bootstrap
57
+
58
+ ## ESLint usage
59
+
60
+ Auto-generated `eslint.config.mjs` uses this package directly:
61
+
62
+ ```js
63
+ import { createArchipelagoConfig } from 'eslint-config'
64
+
65
+ export default createArchipelagoConfig()
66
+ ```
67
+
68
+ ### Override rules
69
+
70
+ You can override any rule in your root `eslint.config.mjs`:
71
+
72
+ ```js
73
+ import { createArchipelagoConfig } from 'eslint-config'
74
+
75
+ export default createArchipelagoConfig({
76
+ name: 'project/overrides',
77
+ rules: {
78
+ 'no-console': 'off',
79
+ 'vue/max-attributes-per-line': 'off',
80
+ },
81
+ })
82
+ ```
83
+
84
+ ## Manual setup (optional)
85
+
86
+ Automatic setup is the default. If needed, you can still run setup manually:
87
+
88
+ ```bash
89
+ node node_modules/eslint-config/tools/setup/install.mjs
90
+ ```
91
+
92
+ ## Hook wrappers reference
93
+
94
+ Root `.hooks` scripts should delegate to installed package path:
95
+
96
+ - `.hooks/pre-commit`
97
+
98
+ ```bash
99
+ #!/usr/bin/env bash
100
+ set -euo pipefail
101
+ cd "$(git rev-parse --show-toplevel)"
102
+ node node_modules/eslint-config/tools/git-hooks/pre-commit.mjs
103
+ ```
104
+
105
+ - `.hooks/prepare-commit-msg`
106
+
107
+ ```bash
108
+ #!/usr/bin/env bash
109
+ set -euo pipefail
110
+ cd "$(git rev-parse --show-toplevel)"
111
+ node node_modules/eslint-config/tools/git-hooks/prepare-commit-msg.mjs "$@"
112
+ ```
113
+
114
+ - `.hooks/commit-msg`
115
+
116
+ ```bash
117
+ #!/usr/bin/env bash
118
+ set -euo pipefail
119
+ cd "$(git rev-parse --show-toplevel)"
120
+ node node_modules/eslint-config/tools/git-hooks/commit-msg.mjs "$1"
121
+ ```
122
+
123
+ - `.hooks/post-commit`
124
+
125
+ ```bash
126
+ #!/usr/bin/env bash
127
+ set -euo pipefail
128
+ cd "$(git rev-parse --show-toplevel)"
129
+ node node_modules/eslint-config/tools/git-hooks/post-commit.mjs
130
+ ```
131
+
132
+ ## Required root scripts
133
+
134
+ ```json
135
+ {
136
+ "scripts": {
137
+ "lint": "pnpm lint:fix",
138
+ "lint:check": "turbo run lint",
139
+ "lint:fix": "((pnpm format || true) && turbo run lint --continue=always -- --fix) || true"
140
+ },
141
+ "lint-staged": {
142
+ "*.{js,ts,tsx,vue}": ["eslint --fix"]
143
+ }
144
+ }
145
+ ```
@@ -0,0 +1,83 @@
1
+ import antfu from '@antfu/eslint-config'
2
+ import perfectionistNatural from 'eslint-plugin-perfectionist'
3
+ import tailwind from 'eslint-plugin-tailwindcss'
4
+
5
+ export function createArchipelagoConfig(...overrides) {
6
+ return antfu(
7
+ {
8
+ formatters: true,
9
+ stylistic: true,
10
+ vue: true,
11
+ typescript: true,
12
+ },
13
+ {
14
+ name: 'archipelago/ignores',
15
+ ignores: [
16
+ '**/node_modules/**',
17
+ '**/.nuxt/**',
18
+ '**/.output/**',
19
+ '**/.turbo/**',
20
+ '**/dist/**',
21
+ '**/coverage/**',
22
+ '**/.next/**',
23
+ '**/public/**',
24
+ ],
25
+ },
26
+ ...tailwind.configs['flat/recommended'],
27
+ {
28
+ name: 'archipelago/perfectionist',
29
+ plugins: {
30
+ perfectionistNatural,
31
+ },
32
+ rules: {
33
+ 'import/order': 'off',
34
+ 'sort-imports': 'off',
35
+ 'perfectionist/sort-imports': ['warn', {
36
+ type: 'alphabetical',
37
+ }],
38
+ 'perfectionist/sort-exports': ['warn', {
39
+ type: 'alphabetical',
40
+ }],
41
+ 'perfectionist/sort-named-imports': ['warn', {
42
+ type: 'alphabetical',
43
+ }],
44
+ 'perfectionist/sort-named-exports': ['warn', {
45
+ type: 'alphabetical',
46
+ }],
47
+ },
48
+ },
49
+ {
50
+ name: 'archipelago/rules',
51
+ rules: {
52
+ 'comma-dangle': ['warn', 'always-multiline'],
53
+ 'no-console': 'warn',
54
+ 'max-statements-per-line': 'warn',
55
+ 'tailwindcss/no-custom-classname': 'off',
56
+ 'vue/multi-word-component-names': 'off',
57
+ 'vue/no-required-prop-with-default': 'off',
58
+ 'vue/html-self-closing': 'off',
59
+ 'vue/html-closing-bracket-spacing': 'off',
60
+ 'vue/no-multiple-template-root': 'off',
61
+ 'vue/max-attributes-per-line': ['error', {
62
+ singleline: 3,
63
+ multiline: 1,
64
+ }],
65
+ 'vue/max-len': ['warn', {
66
+ code: 120,
67
+ ignoreComments: true,
68
+ ignoreUrls: true,
69
+ ignoreStrings: true,
70
+ ignoreTemplateLiterals: true,
71
+ ignoreRegExpLiterals: true,
72
+ ignoreHTMLAttributeValues: true,
73
+ ignoreHTMLTextContents: true,
74
+ }],
75
+ 'object-curly-newline': ['warn', {
76
+ ImportDeclaration: { multiline: true, minProperties: 3 },
77
+ }],
78
+ 'format/prettier': 'warn',
79
+ },
80
+ },
81
+ ...overrides,
82
+ )
83
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@archpublicwebsite/eslint-config",
3
+ "version": "1.0.5",
4
+ "author": "Archipelago International",
5
+ "description": "Reusable ESLint flat config and git-hook toolkit for Archipelago projects",
6
+ "type": "module",
7
+ "main": "./eslint.config.mjs",
8
+ "module": "./eslint.config.mjs",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./eslint.config.mjs",
12
+ "default": "./eslint.config.mjs"
13
+ },
14
+ "./tools/*": "./tools/*"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/ArchipelagoInternational/archi-ui.git",
19
+ "directory": "packages/eslint-config"
20
+ },
21
+ "files": [
22
+ "eslint.config.mjs",
23
+ "tools",
24
+ "README.md"
25
+ ],
26
+ "keywords": [
27
+ "eslint",
28
+ "eslint-config",
29
+ "flat-config",
30
+ "vue",
31
+ "git-hooks"
32
+ ],
33
+ "private": false,
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "scripts": {
38
+ "postinstall": "node ./tools/setup/install.mjs",
39
+ "setup": "node ./tools/setup/install.mjs",
40
+ "prepublishOnly": "npm pack --dry-run",
41
+ "version:patch": "node ../../scripts/bump-version.mjs patch",
42
+ "version:minor": "node ../../scripts/bump-version.mjs minor",
43
+ "version:major": "node ../../scripts/bump-version.mjs major"
44
+ },
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "@antfu/eslint-config": "^4.12.0",
48
+ "@unocss/eslint-config": "^66.5.3",
49
+ "eslint": "^9.0.0",
50
+ "eslint-plugin-format": "^1.0.1",
51
+ "eslint-plugin-perfectionist": "^4.10.1",
52
+ "eslint-plugin-tailwindcss": "^3.17.3",
53
+ "prettier-plugin-tailwindcss": "^0.6.14"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }
@@ -0,0 +1,7 @@
1
+ import { verifyCommitMessageFile } from './verify-commit-message.mjs'
2
+
3
+ const messageFilePath = process.argv[2]
4
+ if (!messageFilePath)
5
+ process.exit(0)
6
+
7
+ verifyCommitMessageFile(messageFilePath)
@@ -0,0 +1,127 @@
1
+ import { getStagedDiffSnippet, getStagedNameStatus } from './shared.mjs'
2
+
3
+ function inferType(paths, addedCount, source) {
4
+ const isDocsOnly = paths.every(path => path.startsWith('docs/') || path.endsWith('.md'))
5
+ if (isDocsOnly)
6
+ return 'docs'
7
+
8
+ const isTestOnly = paths.every(path => /(__tests__|\.test\.|\.spec\.)/.test(path))
9
+ if (isTestOnly)
10
+ return 'test'
11
+
12
+ const isInfraOnly = paths.every(path =>
13
+ path.startsWith('.hooks/')
14
+ || path.startsWith('scripts/')
15
+ || path.endsWith('package.json')
16
+ || path.endsWith('pnpm-lock.yaml')
17
+ || path.endsWith('pnpm-workspace.yaml'),
18
+ )
19
+ if (isInfraOnly)
20
+ return 'chore'
21
+
22
+ if (/(fix|bug|broken|regression|error)/.test(source))
23
+ return 'fix'
24
+
25
+ if (/(button|modal|offer|gallery|room|feature|ui)/.test(source))
26
+ return 'feat'
27
+
28
+ return addedCount > 0 ? 'chore' : 'chore'
29
+ }
30
+
31
+ function inferVerb(addedCount, modifiedCount) {
32
+ if (addedCount > 0 && modifiedCount === 0)
33
+ return 'add'
34
+
35
+ if (addedCount === 0 && modifiedCount > 0)
36
+ return 'update'
37
+
38
+ return 'update'
39
+ }
40
+
41
+ function inferAreas(paths) {
42
+ const mapArea = (path) => {
43
+ if (path.startsWith('packages/ui/'))
44
+ return 'ui'
45
+ if (path.startsWith('apps/nuxt/'))
46
+ return 'nuxt'
47
+ if (path.startsWith('docs/'))
48
+ return 'docs'
49
+ if (path.startsWith('scripts/'))
50
+ return 'scripts'
51
+
52
+ const [first] = path.split('/')
53
+ return first || 'repo'
54
+ }
55
+
56
+ return [...new Set(paths.map(mapArea))]
57
+ }
58
+
59
+ function inferSubject(paths, source) {
60
+ const pathsSource = paths.join(' ').toLowerCase()
61
+ const fullSource = `${pathsSource} ${source}`
62
+
63
+ const topicRules = [
64
+ { test: /(offers?).*(modal)|(modal).*(offers?)/, label: 'offers modal' },
65
+ { test: /(prepare-commit-msg|commit-msg|verifycommit|generatecommitmessage)/, label: 'commit message flow' },
66
+ { test: /(\.hooks\/|pre-commit|post-commit|lint-staged)/, label: 'git hooks' },
67
+ { test: /buttons?/, label: 'buttons' },
68
+ { test: /modal/, label: 'modal behavior' },
69
+ { test: /lint/, label: 'lint workflow' },
70
+ { test: /tailwind|scss|css/, label: 'styles' },
71
+ { test: /gallery/, label: 'gallery section' },
72
+ { test: /offers?/, label: 'offers section' },
73
+ { test: /rooms?/, label: 'rooms section' },
74
+ { test: /contact[\s-]?us/, label: 'contact us section' },
75
+ ]
76
+
77
+ const hit = topicRules.find(rule => rule.test.test(fullSource))
78
+ return hit ? hit.label : ''
79
+ }
80
+
81
+ function makeDescription(verb, fileCount, areas, subject) {
82
+ if (subject)
83
+ return `${verb} ${subject}`
84
+
85
+ const filesPart = `${fileCount} file${fileCount > 1 ? 's' : ''}`
86
+
87
+ if (areas.length === 0)
88
+ return `${verb} ${filesPart}`
89
+
90
+ if (areas.length === 1)
91
+ return `${verb} ${filesPart} in ${areas[0]}`
92
+
93
+ if (areas.length === 2)
94
+ return `${verb} ${filesPart} in ${areas[0]} and ${areas[1]}`
95
+
96
+ return `${verb} ${filesPart} across ${areas[0]}, ${areas[1]}, and more`
97
+ }
98
+
99
+ function normalizeDescription(description) {
100
+ const maxLength = 50
101
+ if (description.length <= maxLength)
102
+ return description
103
+
104
+ return description.slice(0, maxLength).trim().replace(/[.,;:-]$/, '')
105
+ }
106
+
107
+ export function generateCommitMessage() {
108
+ const changed = getStagedNameStatus()
109
+ if (changed.length === 0)
110
+ return 'chore: update repository files'
111
+
112
+ const source = getStagedDiffSnippet()
113
+ const paths = changed.map(item => item.path)
114
+ const addedCount = changed.filter(item => item.status === 'A').length
115
+ const modifiedCount = changed.filter(item => item.status === 'M' || item.status === 'R').length
116
+
117
+ const type = inferType(paths, addedCount, source)
118
+ const verb = inferVerb(addedCount, modifiedCount)
119
+ const areas = inferAreas(paths)
120
+ const subject = inferSubject(paths, source)
121
+ const description = normalizeDescription(makeDescription(verb, changed.length, areas, subject))
122
+
123
+ return `${type}: ${description}`
124
+ }
125
+
126
+ if (import.meta.url === `file://${process.argv[1]}`)
127
+ console.log(generateCommitMessage())
@@ -0,0 +1,15 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { getCommittedCodeFiles, runSafe } from './shared.mjs'
3
+
4
+ const committedFiles = getCommittedCodeFiles().filter(file => existsSync(file))
5
+
6
+ if (committedFiles.length === 0)
7
+ process.exit(0)
8
+
9
+ console.log(`\nRunning post-commit lint auto-fix on ${committedFiles.length} file(s)...`)
10
+ const quotedFiles = committedFiles.map(file => `'${file.replace(/'/g, "'\\''")}'`).join(' ')
11
+ runSafe(`pnpm exec eslint --fix ${quotedFiles}`)
12
+
13
+ const changedAfterFix = runSafe(`git diff -- ${quotedFiles}`)
14
+ if (changedAfterFix)
15
+ console.log('\nLINT FIXES APPLIED. Review and commit follow-up changes if needed.\n')
@@ -0,0 +1,10 @@
1
+ import { hasCommand, run } from './shared.mjs'
2
+
3
+ if (!hasCommand('pnpm')) {
4
+ console.error('\nCOMMIT FAILED: pnpm is required but not found.\n')
5
+ process.exit(1)
6
+ }
7
+
8
+ console.log('\nRunning lint-staged (auto-fix staged files)...')
9
+ run('pnpm lint-staged', { stdio: 'inherit' })
10
+ console.log('\nCOMMIT CHECKS PASSED\n')
@@ -0,0 +1,23 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+ import { generateCommitMessage } from './generate-commit-message.mjs'
3
+
4
+ const messageFile = process.argv[2]
5
+ const source = process.argv[3] || ''
6
+
7
+ if (!messageFile)
8
+ process.exit(0)
9
+
10
+ if (['merge', 'squash', 'commit', 'message'].includes(source))
11
+ process.exit(0)
12
+
13
+ const currentMessage = readFileSync(messageFile, 'utf-8')
14
+ const hasMessage = currentMessage
15
+ .split('\n')
16
+ .some(line => line.trim() && !line.trim().startsWith('#'))
17
+
18
+ if (hasMessage)
19
+ process.exit(0)
20
+
21
+ const autoMessage = generateCommitMessage()
22
+ if (autoMessage)
23
+ writeFileSync(messageFile, `${autoMessage}\n`, 'utf-8')
@@ -0,0 +1,71 @@
1
+ import { execSync } from 'node:child_process'
2
+
3
+ export function run(command, options = {}) {
4
+ const output = execSync(command, {
5
+ encoding: 'utf8',
6
+ stdio: 'pipe',
7
+ ...options,
8
+ })
9
+
10
+ if (output == null) {
11
+ return ''
12
+ }
13
+
14
+ if (typeof output === 'string') {
15
+ return output.trim()
16
+ }
17
+
18
+ return String(output).trim()
19
+ }
20
+
21
+ export function runSafe(command) {
22
+ try {
23
+ return run(command)
24
+ } catch {
25
+ return ''
26
+ }
27
+ }
28
+
29
+ export function getRepoRoot() {
30
+ return run('git rev-parse --show-toplevel')
31
+ }
32
+
33
+ export function hasCommand(name) {
34
+ try {
35
+ run(`command -v ${name}`)
36
+ return true
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ export function getStagedNameStatus() {
43
+ const output = runSafe('git diff --cached --name-status --diff-filter=ACMR')
44
+ if (!output)
45
+ return []
46
+
47
+ return output
48
+ .split('\n')
49
+ .map((line) => {
50
+ const [status = '', ...rest] = line.split(/\s+/)
51
+ return {
52
+ status,
53
+ path: rest.join(' '),
54
+ }
55
+ })
56
+ .filter(item => item.path)
57
+ }
58
+
59
+ export function getStagedDiffSnippet() {
60
+ return runSafe('git diff --cached -- .').slice(0, 15000).toLowerCase()
61
+ }
62
+
63
+ export function getCommittedCodeFiles() {
64
+ const output = runSafe('git diff-tree --no-commit-id --name-only -r HEAD')
65
+ if (!output)
66
+ return []
67
+
68
+ return output
69
+ .split('\n')
70
+ .filter(file => /\.(js|jsx|ts|tsx|vue)$/.test(file))
71
+ }
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ export function isValidCommitMessage(message) {
4
+ const releaseRE = /^v\d/
5
+ const commitRE = /^(revert: )?(feat|fix|docs|refactor|perf|test|build|ci|chore|style|types|workflow|release|deps)(\([a-z0-9-]+\))?: \S.{0,49}$/
6
+ return releaseRE.test(message) || commitRE.test(message)
7
+ }
8
+
9
+ export function verifyCommitMessageFile(messageFilePath) {
10
+ const message = readFileSync(messageFilePath, 'utf-8').trim()
11
+ if (isValidCommitMessage(message))
12
+ return
13
+
14
+ const examples = [
15
+ "feat: add 'comments' option",
16
+ 'fix: handle events on blur (close #28)',
17
+ ]
18
+
19
+ console.error('\nERROR: invalid commit message format.\n')
20
+ console.error('Required format: <type>(optional-scope): <description>')
21
+ console.error('Description max length: 50 characters\n')
22
+ console.error('Examples:')
23
+ examples.forEach(example => console.error(` - ${example}`))
24
+ console.error('\nSee .github/commit-convention.md for details.\n')
25
+ process.exit(1)
26
+ }
27
+
28
+ if (import.meta.url === `file://${process.argv[1]}`) {
29
+ const messageFilePath = process.argv[2]
30
+ if (!messageFilePath)
31
+ process.exit(0)
32
+ verifyCommitMessageFile(messageFilePath)
33
+ }
@@ -0,0 +1,123 @@
1
+ import { execSync } from 'node:child_process'
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ function getProjectRoot() {
6
+ const root = process.env.INIT_CWD || process.cwd()
7
+ if (!existsSync(join(root, 'package.json')))
8
+ return null
9
+ return root
10
+ }
11
+
12
+ function ensureDir(path) {
13
+ if (!existsSync(path))
14
+ mkdirSync(path, { recursive: true })
15
+ }
16
+
17
+ function writeIfMissing(path, content) {
18
+ if (existsSync(path))
19
+ return false
20
+ writeFileSync(path, content, 'utf8')
21
+ return true
22
+ }
23
+
24
+ function ensurePrettierConfig(projectRoot) {
25
+ const prettierPath = join(projectRoot, '.prettierrc')
26
+ const defaults = {
27
+ plugins: ['prettier-plugin-tailwindcss'],
28
+ singleQuote: true,
29
+ printWidth: 120,
30
+ semi: false,
31
+ tabWidth: 2,
32
+ trailingComma: 'es5',
33
+ arrowParens: 'avoid',
34
+ }
35
+
36
+ if (!existsSync(prettierPath)) {
37
+ writeFileSync(prettierPath, `${JSON.stringify(defaults, null, 2)}\n`, 'utf8')
38
+ return
39
+ }
40
+
41
+ try {
42
+ const current = JSON.parse(readFileSync(prettierPath, 'utf8'))
43
+ const plugins = Array.isArray(current.plugins) ? current.plugins : []
44
+ const deduped = [...new Set([...plugins, 'prettier-plugin-tailwindcss'])]
45
+ current.plugins = deduped
46
+ if (typeof current.singleQuote !== 'boolean')
47
+ current.singleQuote = true
48
+ if (typeof current.semi !== 'boolean')
49
+ current.semi = false
50
+ writeFileSync(prettierPath, `${JSON.stringify(current, null, 2)}\n`, 'utf8')
51
+ }
52
+ catch {
53
+ // Keep existing file untouched if it is not JSON.
54
+ }
55
+ }
56
+
57
+ function ensureEslintConfig(projectRoot) {
58
+ const eslintConfigPath = join(projectRoot, 'eslint.config.mjs')
59
+ const content = `import { createArchipelagoConfig } from 'eslint-config'\n\nexport default createArchipelagoConfig({\n name: 'project/overrides',\n rules: {\n // Add your project overrides here\n },\n})\n`
60
+ writeIfMissing(eslintConfigPath, content)
61
+ }
62
+
63
+ function ensureHooks(projectRoot) {
64
+ const hooksDir = join(projectRoot, '.hooks')
65
+ ensureDir(hooksDir)
66
+
67
+ const hooks = {
68
+ 'pre-commit': `#!/usr/bin/env bash
69
+ set -euo pipefail
70
+
71
+ cd "$(git rev-parse --show-toplevel)"
72
+ node node_modules/eslint-config/tools/git-hooks/pre-commit.mjs
73
+ `,
74
+ 'prepare-commit-msg': `#!/usr/bin/env bash
75
+ set -euo pipefail
76
+
77
+ cd "$(git rev-parse --show-toplevel)"
78
+ node node_modules/eslint-config/tools/git-hooks/prepare-commit-msg.mjs "$@"
79
+ `,
80
+ 'commit-msg': `#!/usr/bin/env bash
81
+ set -euo pipefail
82
+
83
+ cd "$(git rev-parse --show-toplevel)"
84
+ node node_modules/eslint-config/tools/git-hooks/commit-msg.mjs "$1"
85
+ `,
86
+ 'post-commit': `#!/usr/bin/env bash
87
+ set -euo pipefail
88
+
89
+ cd "$(git rev-parse --show-toplevel)"
90
+ node node_modules/eslint-config/tools/git-hooks/post-commit.mjs
91
+ `,
92
+ }
93
+
94
+ Object.entries(hooks).forEach(([name, content]) => {
95
+ const path = join(hooksDir, name)
96
+ if (writeIfMissing(path, content))
97
+ chmodSync(path, 0o755)
98
+ })
99
+ }
100
+
101
+ function ensureHooksPath(projectRoot) {
102
+ if (!existsSync(join(projectRoot, '.git')))
103
+ return
104
+ try {
105
+ execSync('git config core.hooksPath .hooks', { cwd: projectRoot, stdio: 'ignore' })
106
+ }
107
+ catch {
108
+ // Ignore setup failures in non-git contexts.
109
+ }
110
+ }
111
+
112
+ function main() {
113
+ const projectRoot = getProjectRoot()
114
+ if (!projectRoot)
115
+ return
116
+
117
+ ensureHooks(projectRoot)
118
+ ensureHooksPath(projectRoot)
119
+ ensureEslintConfig(projectRoot)
120
+ ensurePrettierConfig(projectRoot)
121
+ }
122
+
123
+ main()