@archpublicwebsite/eslint-config 1.0.18 → 1.0.20
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/eslint.config.mjs +92 -7
- package/package.json +1 -1
- package/tools/git-hooks/pre-push.mjs +235 -0
- package/tools/git-hooks/verify-commit-message.mjs +37 -18
- package/tools/security/patterns.mjs +187 -0
- package/tools/security/risks.mjs +259 -0
- package/tools/security/scan.mjs +44 -245
- package/tools/security/scanner.mjs +233 -0
- package/tools/setup/install.mjs +13 -1
package/eslint.config.mjs
CHANGED
|
@@ -37,23 +37,108 @@ export function createArchipelagoConfig(...overrides) {
|
|
|
37
37
|
...tailwind.configs['flat/recommended'],
|
|
38
38
|
|
|
39
39
|
// ── Security — OWASP / supply-chain hardening ─────────────────────────────
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
40
|
+
// Covers: A01 Broken Access Control, A02 Cryptographic Failures,
|
|
41
|
+
// A03 Injection (XSS, eval, prototype pollution),
|
|
42
|
+
// A08 Software and Data Integrity Failures (supply-chain).
|
|
43
|
+
// Risk taxonomy: packages/eslint-config/tools/security/risks.mjs
|
|
44
|
+
// Pre-commit pattern scanner: tools/security/scan.mjs
|
|
45
|
+
// These rules apply to ALL project files.
|
|
43
46
|
{
|
|
44
47
|
name: 'archipelago/security',
|
|
45
48
|
rules: {
|
|
46
|
-
//
|
|
49
|
+
// ── A03: Injection — arbitrary code execution ─────────────────────────
|
|
50
|
+
// eval() / new Function() / setTimeout("string") allow an attacker to
|
|
51
|
+
// execute arbitrary code. Primary dropper entry-points in supply-chain
|
|
52
|
+
// attacks (event-stream, polyfill.io, etc.).
|
|
47
53
|
'no-eval': 'error',
|
|
48
54
|
'no-new-func': 'error',
|
|
49
55
|
'no-implied-eval': 'error',
|
|
50
|
-
|
|
56
|
+
|
|
57
|
+
// ── A03: Injection — XSS via javascript: URI ──────────────────────────
|
|
58
|
+
// javascript: in href/src executes in the page's origin context — XSS.
|
|
51
59
|
'no-script-url': 'error',
|
|
52
|
-
|
|
60
|
+
|
|
61
|
+
// ── A03: Injection — prototype pollution ──────────────────────────────
|
|
62
|
+
// __proto__ / Object.prototype assignment poisons all objects and can
|
|
63
|
+
// escalate to RCE in some server-side Node.js frameworks.
|
|
64
|
+
'no-prototype-builtins': 'error',
|
|
65
|
+
|
|
66
|
+
// ── A03: Injection — DOM XSS sinks ───────────────────────────────────
|
|
67
|
+
// innerHTML / outerHTML / insertAdjacentHTML / document.write let
|
|
68
|
+
// unescaped strings execute as HTML/script in the browser.
|
|
69
|
+
// Use textContent, innerText, or a trusted sanitiser (DOMPurify) instead.
|
|
70
|
+
'no-restricted-syntax': [
|
|
71
|
+
'error',
|
|
72
|
+
// innerHTML / outerHTML assignment
|
|
73
|
+
{
|
|
74
|
+
selector: 'AssignmentExpression[left.property.name=/^(inner|outer)HTML$/]',
|
|
75
|
+
message:
|
|
76
|
+
'[security/xss] innerHTML/outerHTML is a DOM XSS sink — use textContent or sanitise with DOMPurify',
|
|
77
|
+
},
|
|
78
|
+
// insertAdjacentHTML()
|
|
79
|
+
{
|
|
80
|
+
selector: 'CallExpression[callee.property.name="insertAdjacentHTML"]',
|
|
81
|
+
message:
|
|
82
|
+
'[security/xss] insertAdjacentHTML() is a DOM XSS sink — use insertAdjacentText() or sanitise first',
|
|
83
|
+
},
|
|
84
|
+
// document.write / document.writeln
|
|
85
|
+
{
|
|
86
|
+
selector:
|
|
87
|
+
'CallExpression[callee.object.name="document"][callee.property.name=/^write(ln)?$/]',
|
|
88
|
+
message:
|
|
89
|
+
'[security/xss] document.write() is deprecated and a direct XSS sink — remove it',
|
|
90
|
+
},
|
|
91
|
+
// Object.__proto__ assignment (belt-and-suspenders with no-prototype-builtins)
|
|
92
|
+
{
|
|
93
|
+
selector: 'AssignmentExpression[left.property.name="__proto__"]',
|
|
94
|
+
message:
|
|
95
|
+
'[security/prototype-pollution] __proto__ assignment pollutes all objects — use Object.create(null) or Object.assign()',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
|
|
99
|
+
// ── A08: Supply-chain — dangerous globals ─────────────────────────────
|
|
53
100
|
'no-restricted-globals': [
|
|
54
101
|
'error',
|
|
55
|
-
{
|
|
102
|
+
{
|
|
103
|
+
name: 'eval',
|
|
104
|
+
message: '[security/rce] eval() is a security risk — use a safe alternative',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'isFinite',
|
|
108
|
+
message: 'Use Number.isFinite() instead of the global isFinite()',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'isNaN',
|
|
112
|
+
message: 'Use Number.isNaN() instead of the global isNaN()',
|
|
113
|
+
},
|
|
56
114
|
],
|
|
115
|
+
|
|
116
|
+
// ── A02: Cryptographic failures — weak algorithms ─────────────────────
|
|
117
|
+
// MD5 and SHA-1 are cryptographically broken.
|
|
118
|
+
'no-restricted-imports': [
|
|
119
|
+
'error',
|
|
120
|
+
{
|
|
121
|
+
name: 'md5',
|
|
122
|
+
message: '[security/crypto] MD5 is cryptographically broken — use SHA-256 or better',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'sha1',
|
|
126
|
+
message: '[security/crypto] SHA-1 is cryptographically broken — use SHA-256 or better',
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// ── Security — Vue-specific XSS hardening ─────────────────────────────────
|
|
133
|
+
// v-html renders raw HTML without escaping. If the bound value contains
|
|
134
|
+
// user-controlled content it is a direct XSS sink.
|
|
135
|
+
// Warn (not error): v-html is legitimate for trusted CMS content, but every
|
|
136
|
+
// usage must be explicitly reviewed.
|
|
137
|
+
{
|
|
138
|
+
name: 'archipelago/security-vue',
|
|
139
|
+
files: ['**/*.vue'],
|
|
140
|
+
rules: {
|
|
141
|
+
'vue/no-v-html': 'warn',
|
|
57
142
|
},
|
|
58
143
|
},
|
|
59
144
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @archipelago/pre-push
|
|
4
|
+
*
|
|
5
|
+
* Full-branch security gate — runs before code leaves the local machine.
|
|
6
|
+
* Stricter than the pre-commit scanner: operates over ALL committed JS/TS/Vue
|
|
7
|
+
* files in the push range and checks all package.json deps for known CVEs
|
|
8
|
+
* and typosquatting.
|
|
9
|
+
*
|
|
10
|
+
* ┌────────────────────────────────────────────────────────┐
|
|
11
|
+
* │ What is checked │
|
|
12
|
+
* │ 1. All .js/.ts/.vue files in the push range against │
|
|
13
|
+
* │ all FILE_PATTERNS (eval, XSS, secrets, SSRF, …) │
|
|
14
|
+
* │ 2. pnpm audit --audit-level=high (CVE scan) │
|
|
15
|
+
* │ 3. Typosquatting check on all package.json deps │
|
|
16
|
+
* └────────────────────────────────────────────────────────┘
|
|
17
|
+
*
|
|
18
|
+
* Environment flags:
|
|
19
|
+
* SKIP_SECURITY_SCAN=1 Bypass all checks (logged loudly to stderr)
|
|
20
|
+
* SKIP_PUSH_AUDIT=1 Skip the pnpm audit step only
|
|
21
|
+
*
|
|
22
|
+
* Git push stdin lines: <local-ref> <local-sha1> <remote-ref> <remote-sha1>
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
26
|
+
import { extname, join } from 'node:path'
|
|
27
|
+
import { SCAN_EXTENSIONS } from '../security/patterns.mjs'
|
|
28
|
+
import { partitionFindings, sortFindings } from '../security/risks.mjs'
|
|
29
|
+
import {
|
|
30
|
+
BOLD,
|
|
31
|
+
DIM,
|
|
32
|
+
GRN,
|
|
33
|
+
R,
|
|
34
|
+
YLW,
|
|
35
|
+
collectObfuscatedLineFindings,
|
|
36
|
+
collectPatternFindings,
|
|
37
|
+
detectTyposquat,
|
|
38
|
+
isConfigFile,
|
|
39
|
+
printFileFinding,
|
|
40
|
+
printScanFooter,
|
|
41
|
+
printScanHeader,
|
|
42
|
+
printSupplyFinding,
|
|
43
|
+
shouldSkip,
|
|
44
|
+
} from '../security/scanner.mjs'
|
|
45
|
+
import { getRepoRoot, runSafe } from './shared.mjs'
|
|
46
|
+
|
|
47
|
+
// ─── Parse git push stdin refs ─────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const ZERO_SHA = '0000000000000000000000000000000000000000'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse git push stdin lines and return push ranges.
|
|
53
|
+
* @returns {{ localSha: string, remoteSha: string }[]}
|
|
54
|
+
*/
|
|
55
|
+
function parsePushRefs() {
|
|
56
|
+
let stdin = ''
|
|
57
|
+
try { stdin = readFileSync('/dev/stdin', 'utf8') }
|
|
58
|
+
catch { return [] }
|
|
59
|
+
|
|
60
|
+
return stdin.trim().split('\n').filter(Boolean).map((line) => {
|
|
61
|
+
const parts = line.trim().split(/\s+/)
|
|
62
|
+
return { localSha: parts[1] ?? '', remoteSha: parts[3] ?? ZERO_SHA }
|
|
63
|
+
}).filter(({ localSha }) => localSha && localSha !== ZERO_SHA)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Collect files in push range ───────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {{ localSha: string, remoteSha: string }[]} refs
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
72
|
+
function collectPushFiles(refs) {
|
|
73
|
+
const seen = new Set()
|
|
74
|
+
for (const { localSha, remoteSha } of refs) {
|
|
75
|
+
const range = remoteSha === ZERO_SHA ? localSha : `${remoteSha}..${localSha}`
|
|
76
|
+
const output = runSafe(`git diff-tree --no-commit-id --name-only -r ${range}`)
|
|
77
|
+
if (!output) continue
|
|
78
|
+
for (const f of output.split('\n').filter(Boolean)) {
|
|
79
|
+
if (SCAN_EXTENSIONS.has(extname(f))) seen.add(f)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...seen]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Scan committed files ──────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string[]} files
|
|
89
|
+
* @param {string} repoRoot
|
|
90
|
+
* @returns {import('../security/scanner.mjs').FileFinding[]}
|
|
91
|
+
*/
|
|
92
|
+
function scanFiles(files, repoRoot) {
|
|
93
|
+
if (files.length === 0) return []
|
|
94
|
+
|
|
95
|
+
process.stdout.write(`\nScanning ${files.length} file(s) in push range...\n`)
|
|
96
|
+
|
|
97
|
+
const findings = []
|
|
98
|
+
let skipped = 0
|
|
99
|
+
|
|
100
|
+
for (const filePath of files) {
|
|
101
|
+
if (shouldSkip(filePath)) { skipped++; continue }
|
|
102
|
+
|
|
103
|
+
const content = runSafe(`git show "HEAD:${filePath}"`)
|
|
104
|
+
|| (() => {
|
|
105
|
+
const abs = join(repoRoot, filePath)
|
|
106
|
+
return existsSync(abs) ? readFileSync(abs, 'utf8') : ''
|
|
107
|
+
})()
|
|
108
|
+
|
|
109
|
+
if (!content) continue
|
|
110
|
+
|
|
111
|
+
const lines = content.split('\n')
|
|
112
|
+
findings.push(
|
|
113
|
+
...collectPatternFindings(lines, isConfigFile(filePath), filePath),
|
|
114
|
+
...collectObfuscatedLineFindings(lines, filePath),
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (skipped > 0) {
|
|
119
|
+
process.stdout.write(`Skipped ${skipped} self-referential file(s).\n`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return findings
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Dependency checks ─────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Scan all package.json deps in the repo root for typosquatted names.
|
|
129
|
+
* @param {string} repoRoot
|
|
130
|
+
*/
|
|
131
|
+
function scanDependencies(repoRoot) {
|
|
132
|
+
const pkgJsonPath = join(repoRoot, 'package.json')
|
|
133
|
+
if (!existsSync(pkgJsonPath)) return []
|
|
134
|
+
|
|
135
|
+
let pkg
|
|
136
|
+
try { pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) }
|
|
137
|
+
catch { return [] }
|
|
138
|
+
|
|
139
|
+
const depFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
|
|
140
|
+
return depFields.flatMap((field) => {
|
|
141
|
+
const deps = pkg[field]
|
|
142
|
+
if (!deps || typeof deps !== 'object') return []
|
|
143
|
+
return Object.keys(deps).flatMap((name) => {
|
|
144
|
+
const similar = detectTyposquat(name)
|
|
145
|
+
return similar
|
|
146
|
+
? [{
|
|
147
|
+
pkg: name,
|
|
148
|
+
patternId: 'typosquat',
|
|
149
|
+
severity: 'high',
|
|
150
|
+
message: `Possible typosquat of "${similar}" in ${field} — verify the package name`,
|
|
151
|
+
lineContent: `"${name}" is 1 edit away from "${similar}"`,
|
|
152
|
+
category: 'supply-chain',
|
|
153
|
+
}]
|
|
154
|
+
: []
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Run pnpm audit and surface a single finding if vulnerabilities are detected.
|
|
161
|
+
* @returns {object[]}
|
|
162
|
+
*/
|
|
163
|
+
function runAudit() {
|
|
164
|
+
if (process.env.SKIP_PUSH_AUDIT === '1') {
|
|
165
|
+
process.stdout.write(`${DIM}Skipping pnpm audit (SKIP_PUSH_AUDIT=1)${R}\n`)
|
|
166
|
+
return []
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
process.stdout.write(`\nRunning pnpm audit --audit-level=high...\n`)
|
|
170
|
+
const result = runSafe('pnpm audit --audit-level=high 2>&1')
|
|
171
|
+
if (!result) return []
|
|
172
|
+
|
|
173
|
+
const lines = result.split('\n').filter(Boolean)
|
|
174
|
+
const hasVulns = /vulnerabilit(?:y|ies)|moderate|high|critical/i.test(result)
|
|
175
|
+
|
|
176
|
+
if (hasVulns) {
|
|
177
|
+
console.log(`\n${BOLD}pnpm audit output:${R}`)
|
|
178
|
+
console.log(`${DIM}${result}${R}`)
|
|
179
|
+
return [{
|
|
180
|
+
pkg: 'pnpm-audit',
|
|
181
|
+
patternId: 'known-cve',
|
|
182
|
+
severity: 'high',
|
|
183
|
+
message: `pnpm audit detected known CVEs — run 'pnpm audit --fix' or pin safe versions`,
|
|
184
|
+
lineContent: lines.find(l => /vulnerabilit/i.test(l)) ?? result.slice(0, 120),
|
|
185
|
+
category: 'supply-chain',
|
|
186
|
+
}]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
process.stdout.write(`${GRN}pnpm audit: no known vulnerabilities found.${R}\n`)
|
|
190
|
+
return []
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── Entry point ───────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
function main() {
|
|
196
|
+
if (process.env.SKIP_SECURITY_SCAN === '1') {
|
|
197
|
+
process.stderr.write(
|
|
198
|
+
`\n${YLW}${BOLD}⚠ SKIP_SECURITY_SCAN=1 — pre-push security checks bypassed.${R}\n`
|
|
199
|
+
+ `${YLW} This bypass is intentional and has been logged.${R}\n\n`,
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const repoRoot = getRepoRoot()
|
|
205
|
+
const refs = parsePushRefs()
|
|
206
|
+
|
|
207
|
+
printScanHeader('@archipelago/pre-push', 'full-branch security gate')
|
|
208
|
+
|
|
209
|
+
const files = refs.length > 0
|
|
210
|
+
? collectPushFiles(refs)
|
|
211
|
+
: (runSafe('git diff-tree --no-commit-id --name-only -r HEAD') || '')
|
|
212
|
+
.split('\n')
|
|
213
|
+
.filter(f => SCAN_EXTENSIONS.has(extname(f)))
|
|
214
|
+
|
|
215
|
+
const fileFindings = scanFiles(files, repoRoot)
|
|
216
|
+
const depsFindings = scanDependencies(repoRoot)
|
|
217
|
+
const auditFindings = runAudit()
|
|
218
|
+
|
|
219
|
+
const allFindings = sortFindings([...fileFindings, ...depsFindings, ...auditFindings])
|
|
220
|
+
const { blocking, warnings } = partitionFindings(allFindings)
|
|
221
|
+
|
|
222
|
+
if (fileFindings.length > 0) {
|
|
223
|
+
console.log(`\n${BOLD}Source-file findings:${R}`)
|
|
224
|
+
fileFindings.forEach(printFileFinding)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (depsFindings.length > 0 || auditFindings.length > 0) {
|
|
228
|
+
console.log(`\n${BOLD}Dependency findings:${R}`)
|
|
229
|
+
;[...depsFindings, ...auditFindings].forEach(printSupplyFinding)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
printScanFooter(blocking, warnings, 'PUSH BLOCKED', 'SKIP_SECURITY_SCAN=1 git push …')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
main()
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
|
|
3
|
+
const COMMIT_RE = /^(revert: )?(feat|fix|docs|refactor|perf|test|build|ci|chore|style|types|workflow|release|deps)(\([a-z0-9-]+\))?: \S.*$/
|
|
4
|
+
const RELEASE_RE = /^v\d/
|
|
5
|
+
|
|
6
|
+
const EXAMPLES = [
|
|
7
|
+
"feat: add 'comments' option",
|
|
8
|
+
'fix: handle events on blur (close #28)',
|
|
9
|
+
]
|
|
10
|
+
|
|
3
11
|
export function isValidCommitMessage(message) {
|
|
4
|
-
|
|
5
|
-
const commitRE = /^(revert: )?(feat|fix|docs|refactor|perf|test|build|ci|chore|style|types|workflow|release|deps)(\([a-z0-9-]+\))?: \S.*$/
|
|
6
|
-
return releaseRE.test(message) || commitRE.test(message)
|
|
12
|
+
return RELEASE_RE.test(message) || COMMIT_RE.test(message)
|
|
7
13
|
}
|
|
8
14
|
|
|
9
15
|
function getCommitSubject(rawMessage) {
|
|
@@ -13,36 +19,49 @@ function getCommitSubject(rawMessage) {
|
|
|
13
19
|
.find(line => line && !line.startsWith('#')) || ''
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
function
|
|
17
|
-
return !subject
|
|
22
|
+
function isSkippableSubject(subject) {
|
|
23
|
+
return !subject
|
|
24
|
+
|| subject.startsWith('Merge ')
|
|
25
|
+
|| subject.startsWith('fixup! ')
|
|
26
|
+
|| subject.startsWith('squash! ')
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Verify a commit message file against the conventional-commit format.
|
|
31
|
+
*
|
|
32
|
+
* Behaviour is controlled by the environment:
|
|
33
|
+
* - Default (no env var): warn to stderr but never block the commit.
|
|
34
|
+
* - ARCHI_STRICT_COMMIT_MSG=1: enforce the format and call process.exit(1)
|
|
35
|
+
* on an invalid message so CI or git hooks can block the commit.
|
|
36
|
+
* - STRICT_COMMIT_MSG=1: alias for ARCHI_STRICT_COMMIT_MSG.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} messageFilePath Path to the COMMIT_EDITMSG file.
|
|
39
|
+
*/
|
|
20
40
|
export function verifyCommitMessageFile(messageFilePath) {
|
|
21
41
|
const rawMessage = readFileSync(messageFilePath, 'utf-8')
|
|
22
42
|
const subject = getCommitSubject(rawMessage)
|
|
23
43
|
|
|
24
|
-
if (
|
|
25
|
-
|
|
44
|
+
if (isSkippableSubject(subject)) return
|
|
45
|
+
if (isValidCommitMessage(subject)) return
|
|
26
46
|
|
|
27
|
-
|
|
28
|
-
|
|
47
|
+
const isStrict = process.env.ARCHI_STRICT_COMMIT_MSG === '1'
|
|
48
|
+
|| process.env.STRICT_COMMIT_MSG === '1'
|
|
29
49
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
'fix: handle events on blur (close #28)',
|
|
33
|
-
]
|
|
50
|
+
const prefix = isStrict ? '\nERROR' : '\nSUGGESTION'
|
|
51
|
+
const suffix = isStrict ? 'See README.md for details.\n' : 'See README.md (Auto commit message flow) for details.\n'
|
|
34
52
|
|
|
35
|
-
console.warn(
|
|
53
|
+
console.warn(`${prefix}: commit message does not follow conventional-commit format.\n`)
|
|
36
54
|
console.warn('Required format: <type>(optional-scope): <description>')
|
|
37
55
|
console.warn('Suggested subject length: around 100 characters\n')
|
|
38
56
|
console.warn('Examples:')
|
|
39
|
-
|
|
40
|
-
console.warn(
|
|
57
|
+
EXAMPLES.forEach(example => console.warn(` - ${example}`))
|
|
58
|
+
console.warn(`\n${suffix}`)
|
|
59
|
+
|
|
60
|
+
if (isStrict) process.exit(1)
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
44
64
|
const messageFilePath = process.argv[2]
|
|
45
|
-
if (!messageFilePath)
|
|
46
|
-
process.exit(0)
|
|
65
|
+
if (!messageFilePath) process.exit(0)
|
|
47
66
|
verifyCommitMessageFile(messageFilePath)
|
|
48
67
|
}
|