@itcase/config 1.6.72 → 1.6.73
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/commitlint/commitlintCommon.js +38 -1
- package/package.json +1 -1
- package/smart-commit/cli.js +85 -5
|
@@ -16,7 +16,44 @@ export const commonRules = {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export const promptSubjectDescription =
|
|
19
|
-
'
|
|
19
|
+
'Short imperative description in lower-case (no camelCase: use "public dir" not "publicDir")'
|
|
20
|
+
|
|
21
|
+
export const promptSubjectRules = [
|
|
22
|
+
'lower-case only (commitlint subject-case)',
|
|
23
|
+
'no camelCase: write "public dir", not "publicDir"',
|
|
24
|
+
'imperative mood, no trailing period',
|
|
25
|
+
'single line — no body or footer',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Splits camelCase/PascalCase words so subject-case lower-case can pass.
|
|
30
|
+
* @param {string} subject
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeSubjectCase(subject) {
|
|
34
|
+
return String(subject || '')
|
|
35
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
36
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/\s+/g, ' ')
|
|
39
|
+
.trim()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalizes full conventional commit header for commitlint subject-case.
|
|
44
|
+
* @param {string} message
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeCommitSubject(message) {
|
|
48
|
+
const header = String(message || '')
|
|
49
|
+
.split('\n')[0]
|
|
50
|
+
.trim()
|
|
51
|
+
const match = header.match(/^(\w+)\(([^)]+)\):\s*(.+)$/)
|
|
52
|
+
if (!match) return header
|
|
53
|
+
|
|
54
|
+
const [, type, scope, subject] = match
|
|
55
|
+
return `${type}(${scope}): ${normalizeSubjectCase(subject)}`
|
|
56
|
+
}
|
|
20
57
|
|
|
21
58
|
/**
|
|
22
59
|
* Builds flat scope-enum list from SCOPES.
|
package/package.json
CHANGED
package/smart-commit/cli.js
CHANGED
|
@@ -6,9 +6,15 @@ import { stdin as input, stdout as output } from 'node:process'
|
|
|
6
6
|
import readline from 'node:readline/promises'
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
8
8
|
|
|
9
|
+
import lint from '@commitlint/lint'
|
|
10
|
+
import load from '@commitlint/load'
|
|
9
11
|
import { logUpdateStderr } from 'log-update'
|
|
10
12
|
|
|
11
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
buildScopeEnum,
|
|
15
|
+
normalizeCommitSubject,
|
|
16
|
+
promptSubjectRules,
|
|
17
|
+
} from '../commitlint/commitlintCommon.js'
|
|
12
18
|
import { getBananaFrame } from './dancing-banana.js'
|
|
13
19
|
|
|
14
20
|
const PACKAGE_NAME = '@itcase/config'
|
|
@@ -76,6 +82,7 @@ async function main(argv) {
|
|
|
76
82
|
),
|
|
77
83
|
)
|
|
78
84
|
const plans = parsePlan(raw, files)
|
|
85
|
+
await validateAndNormalizePlans(cwd, plans)
|
|
79
86
|
|
|
80
87
|
process.stdout.write('\nCommits:\n')
|
|
81
88
|
for (const plan of plans) process.stdout.write(` • ${plan.message}\n`)
|
|
@@ -90,16 +97,45 @@ async function main(argv) {
|
|
|
90
97
|
return
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
|
|
100
|
+
const activePlans = plans
|
|
101
|
+
.map((plan) => ({
|
|
102
|
+
...plan,
|
|
103
|
+
files: plan.files.filter((file) => fileHasChanges(cwd, file)),
|
|
104
|
+
}))
|
|
105
|
+
.filter((plan) => plan.files.length)
|
|
106
|
+
|
|
107
|
+
if (!activePlans.length) {
|
|
108
|
+
process.stdout.write('No changes left to commit (already committed?).\n')
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (activePlans.length !== plans.length) {
|
|
113
|
+
process.stdout.write(
|
|
114
|
+
`Skipping ${plans.length - activePlans.length} empty commit(s).\n`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let created = 0
|
|
119
|
+
for (const plan of activePlans) {
|
|
94
120
|
git(cwd, ['add', '--', ...plan.files])
|
|
121
|
+
const staged = lines(cwd, ['diff', '--cached', '--name-only'])
|
|
122
|
+
const stagedForPlan = plan.files.filter((file) => staged.includes(file))
|
|
123
|
+
if (!stagedForPlan.length) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Nothing staged for commit: ${plan.message}\n` +
|
|
126
|
+
`Files: ${plan.files.join(', ')}`,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
95
130
|
const commit = spawnSync('git', ['commit', '-m', plan.message], {
|
|
96
131
|
cwd: cwd,
|
|
97
132
|
encoding: 'utf8',
|
|
98
133
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
99
134
|
})
|
|
100
135
|
if (commit.status) throw new Error(`git commit failed: ${plan.message}`)
|
|
136
|
+
created++
|
|
101
137
|
}
|
|
102
|
-
process.stdout.write(`Created ${
|
|
138
|
+
process.stdout.write(`Created ${created} commit(s).\n`)
|
|
103
139
|
}
|
|
104
140
|
|
|
105
141
|
function parseArgs(argv) {
|
|
@@ -267,7 +303,9 @@ function buildPrompt(scopes, types, files, cwd, type) {
|
|
|
267
303
|
Return ONLY JSON: {"commits":[{"message":"type(scope): subject","files":["a.ts"]}]}
|
|
268
304
|
Types: ${types.join(', ')}
|
|
269
305
|
Scopes (exact): ${buildScopeEnum(scopes).join(', ')}
|
|
270
|
-
Subject:
|
|
306
|
+
Subject rules:
|
|
307
|
+
${promptSubjectRules.map((rule) => `- ${rule}`).join('\n')}
|
|
308
|
+
Prefer one commit when changes belong to the same feature.
|
|
271
309
|
Split by scope. Each listed file in exactly one commit.
|
|
272
310
|
Do not add Co-authored-by or Made-with trailers.
|
|
273
311
|
${type ? `Prefer type "${type}".\n` : ''}SCOPES: ${JSON.stringify(scopes)}
|
|
@@ -491,7 +529,49 @@ function normalizeCommitMessage(message) {
|
|
|
491
529
|
.filter((line) => !/^Made-with:/i.test(line))
|
|
492
530
|
|
|
493
531
|
const subject = (lines[0] || '').replace(/\s+Co-authored-by:.*$/i, '').trim()
|
|
494
|
-
return subject
|
|
532
|
+
return normalizeCommitSubject(subject)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function validateAndNormalizePlans(cwd, plans) {
|
|
536
|
+
const loaded = await load({}, cwd)
|
|
537
|
+
const lintOptions = {
|
|
538
|
+
defaultIgnores: loaded.defaultIgnores,
|
|
539
|
+
ignores: loaded.ignores,
|
|
540
|
+
parserPreset: loaded.parserPreset,
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
for (const plan of plans) {
|
|
544
|
+
plan.message = normalizeCommitSubject(plan.message)
|
|
545
|
+
const report = await lint(plan.message, loaded.rules, lintOptions)
|
|
546
|
+
if (report.valid) continue
|
|
547
|
+
|
|
548
|
+
const errors =
|
|
549
|
+
report.errors?.map((error) => error.message).join('; ') || 'invalid'
|
|
550
|
+
throw new Error(
|
|
551
|
+
`Commit message failed commitlint:\n ${plan.message}\n ${errors}\n` +
|
|
552
|
+
'Use lower-case subject without camelCase (e.g. "public dir" not "publicDir").',
|
|
553
|
+
)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function fileHasChanges(cwd, file) {
|
|
558
|
+
const filePath = path.join(cwd, file)
|
|
559
|
+
if (!fs.existsSync(filePath)) return false
|
|
560
|
+
|
|
561
|
+
const tracked =
|
|
562
|
+
spawnSync('git', ['ls-files', '--error-unmatch', '--', file], {
|
|
563
|
+
cwd: cwd,
|
|
564
|
+
encoding: 'utf8',
|
|
565
|
+
}).status === 0
|
|
566
|
+
|
|
567
|
+
if (!tracked) return true
|
|
568
|
+
|
|
569
|
+
return (
|
|
570
|
+
spawnSync('git', ['diff', '--quiet', 'HEAD', '--', file], {
|
|
571
|
+
cwd: cwd,
|
|
572
|
+
encoding: 'utf8',
|
|
573
|
+
}).status === 1
|
|
574
|
+
)
|
|
495
575
|
}
|
|
496
576
|
|
|
497
577
|
async function confirm(count) {
|