@itcase/config 1.6.78 → 1.6.80
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 +1 -1
- package/lint-staged/index.js +5 -2
- package/lint-staged-ts/index.js +5 -2
- package/package.json +1 -1
- package/smart-commit/cli.js +215 -49
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ npm install --save-dev commitizen @commitlint/cz-commitlint
|
|
|
87
87
|
|
|
88
88
|
## Smart commit
|
|
89
89
|
|
|
90
|
-
`smart-commit` — Cursor Agent (`ask`) планирует коммиты по `SCOPES`, печатает сообщения, спрашивает `[y/N]`. Перед коммитом ждёт и снимает устаревший `.git/index.lock`, если его никто не держит.
|
|
90
|
+
`smart-commit` — Cursor Agent (`ask`) планирует коммиты по `SCOPES`, печатает сообщения, спрашивает `[y/N]`. Перед коммитом ждёт и снимает устаревший `.git/index.lock`, если его никто не держит. Lockfile (`package-lock.json` и аналоги) всегда кладётся в тот же коммит, что и соседний `package.json`; при одном только bump deps агент не вызывается.
|
|
91
91
|
|
|
92
92
|
```text
|
|
93
93
|
[yellow pixel banana animation]
|
package/lint-staged/index.js
CHANGED
|
@@ -9,8 +9,11 @@ export default {
|
|
|
9
9
|
'package.json': (files) => {
|
|
10
10
|
// Проверяем, есть ли package.json в корне репозитория
|
|
11
11
|
const rootPackageJson = resolve(process.cwd(), 'package.json')
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const isRoot = files.some((file) => resolve(file) === rootPackageJson)
|
|
13
|
+
if (existsSync(rootPackageJson) && isRoot) {
|
|
14
|
+
// lock пишется командой, но glob только package.json — без git add
|
|
15
|
+
// он останется вне коммита (типичный leftover после bump deps)
|
|
16
|
+
return ['npm install --package-lock-only', 'git add -- package-lock.json']
|
|
14
17
|
}
|
|
15
18
|
return 'true' // Пропускаем, если не найден
|
|
16
19
|
},
|
package/lint-staged-ts/index.js
CHANGED
|
@@ -17,8 +17,11 @@ export default {
|
|
|
17
17
|
'package.json': (files) => {
|
|
18
18
|
// Проверяем, есть ли package.json в корне репозитория
|
|
19
19
|
const rootPackageJson = resolve(process.cwd(), 'package.json')
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const isRoot = files.some((file) => resolve(file) === rootPackageJson)
|
|
21
|
+
if (existsSync(rootPackageJson) && isRoot) {
|
|
22
|
+
// lock пишется командой, но glob только package.json — без git add
|
|
23
|
+
// он останется вне коммита (типичный leftover после bump deps)
|
|
24
|
+
return ['npm install --package-lock-only', 'git add -- package-lock.json']
|
|
22
25
|
}
|
|
23
26
|
return 'true' // Пропускаем, если не найден
|
|
24
27
|
},
|
package/package.json
CHANGED
package/smart-commit/cli.js
CHANGED
|
@@ -22,6 +22,14 @@ const PACKAGE_NAME = '@itcase/config'
|
|
|
22
22
|
const CLI_RELATIVE = 'smart-commit/cli.js'
|
|
23
23
|
|
|
24
24
|
const MAX_DIFF = 80_000
|
|
25
|
+
const LOCKFILE_NAMES = new Set([
|
|
26
|
+
'package-lock.json',
|
|
27
|
+
'npm-shrinkwrap.json',
|
|
28
|
+
'yarn.lock',
|
|
29
|
+
'pnpm-lock.yaml',
|
|
30
|
+
'bun.lock',
|
|
31
|
+
'bun.lockb',
|
|
32
|
+
])
|
|
25
33
|
const AGENT_TIMEOUT_MS = 120_000
|
|
26
34
|
const AGENT_STEP_MS = 2_500
|
|
27
35
|
const NYAN_FRAME_MS = 90
|
|
@@ -77,24 +85,28 @@ async function main(argv) {
|
|
|
77
85
|
return
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
let plans = tryFastPlan(changedFiles, cwd, scopes, types)
|
|
89
|
+
if (!plans) {
|
|
90
|
+
const prompt = buildPrompt(scopes, types, changedFiles, cwd, option.type)
|
|
91
|
+
const raw = await withAgentProgress(
|
|
92
|
+
{
|
|
93
|
+
fileCount: changedFiles.length,
|
|
94
|
+
sprite: pickSprite(option.sprite),
|
|
95
|
+
verbose: option.verbose,
|
|
96
|
+
},
|
|
97
|
+
(progress) =>
|
|
98
|
+
askAgent(
|
|
99
|
+
cwd,
|
|
100
|
+
option.model || process.env.SMART_COMMIT_MODEL || '',
|
|
101
|
+
prompt,
|
|
102
|
+
option.verbose,
|
|
103
|
+
progress.onAgentActivity,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
plans = parsePlan(raw, changedFiles)
|
|
107
|
+
plans = assignOmittedFiles(plans, changedFiles)
|
|
108
|
+
plans = attachLockfiles(plans, changedFiles)
|
|
109
|
+
}
|
|
98
110
|
await validateAndNormalizePlans(cwd, plans)
|
|
99
111
|
|
|
100
112
|
process.stdout.write('\nCommits:\n')
|
|
@@ -130,14 +142,25 @@ async function main(argv) {
|
|
|
130
142
|
|
|
131
143
|
ensureGitIndexReady(cwd)
|
|
132
144
|
|
|
145
|
+
const total = activePlans.length
|
|
146
|
+
process.stdout.write(`Creating ${total} commit(s)…\n`)
|
|
147
|
+
|
|
133
148
|
let created = 0
|
|
134
|
-
for (
|
|
149
|
+
for (let index = 0; index < total; index++) {
|
|
150
|
+
const plan = activePlans[index]
|
|
151
|
+
const step = `[${index + 1}/${total}]`
|
|
135
152
|
const planFiles = plan.files.filter((file) => fileHasChanges(cwd, file))
|
|
136
153
|
if (!planFiles.length) {
|
|
137
|
-
process.stdout.write(
|
|
154
|
+
process.stdout.write(`${step} skip empty: ${plan.message}\n`)
|
|
138
155
|
continue
|
|
139
156
|
}
|
|
140
157
|
|
|
158
|
+
const startedAt = Date.now()
|
|
159
|
+
const fileLabel =
|
|
160
|
+
planFiles.length === 1 ? '1 file' : `${planFiles.length} files`
|
|
161
|
+
process.stdout.write(`${step} ${plan.message}\n`)
|
|
162
|
+
process.stdout.write(` staging ${fileLabel}…\n`)
|
|
163
|
+
|
|
141
164
|
// Drop leftover index entries so only this plan's files are committed.
|
|
142
165
|
git(cwd, ['reset', '-q', 'HEAD'])
|
|
143
166
|
stageFiles(cwd, planFiles)
|
|
@@ -150,6 +173,7 @@ async function main(argv) {
|
|
|
150
173
|
)
|
|
151
174
|
}
|
|
152
175
|
|
|
176
|
+
process.stdout.write(' git commit (husky)…\n')
|
|
153
177
|
try {
|
|
154
178
|
git(cwd, ['commit', '-m', plan.message], { inherit: true })
|
|
155
179
|
} catch (error) {
|
|
@@ -158,6 +182,7 @@ async function main(argv) {
|
|
|
158
182
|
})
|
|
159
183
|
}
|
|
160
184
|
created++
|
|
185
|
+
process.stdout.write(` done (${formatElapsed(startedAt)})\n`)
|
|
161
186
|
}
|
|
162
187
|
process.stdout.write(`Created ${created} commit(s).\n`)
|
|
163
188
|
|
|
@@ -332,6 +357,15 @@ function listFiles(cwd, option) {
|
|
|
332
357
|
]
|
|
333
358
|
}
|
|
334
359
|
|
|
360
|
+
function isLockfile(file) {
|
|
361
|
+
return LOCKFILE_NAMES.has(path.basename(file))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function siblingManifest(file) {
|
|
365
|
+
const directory = path.posix.dirname(file)
|
|
366
|
+
return directory === '.' ? 'package.json' : `${directory}/package.json`
|
|
367
|
+
}
|
|
368
|
+
|
|
335
369
|
function buildPrompt(scopes, types, files, cwd, type) {
|
|
336
370
|
const statusByFile = new Map(
|
|
337
371
|
lines(cwd, ['status', '--short', '--porcelain', '--', ...files]).map(
|
|
@@ -339,10 +373,9 @@ function buildPrompt(scopes, types, files, cwd, type) {
|
|
|
339
373
|
),
|
|
340
374
|
)
|
|
341
375
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.join('\n\n')
|
|
376
|
+
const sourceFiles = files.filter((file) => !isLockfile(file))
|
|
377
|
+
const lockFiles = files.filter((file) => isLockfile(file))
|
|
378
|
+
let diff = buildSourceDiff(cwd, sourceFiles)
|
|
346
379
|
if (diff.length > MAX_DIFF) diff = `${diff.slice(0, MAX_DIFF)}\n…truncated`
|
|
347
380
|
|
|
348
381
|
return `Plan git commits. Do not edit files or run git commit.
|
|
@@ -353,46 +386,57 @@ Subject rules:
|
|
|
353
386
|
${promptSubjectRules.map((rule) => `- ${rule}`).join('\n')}
|
|
354
387
|
Prefer one commit when changes belong to the same feature.
|
|
355
388
|
Split by scope. Each listed file in exactly one commit — including new/untracked files (??).
|
|
389
|
+
Put each lockfile in the same commit as its sibling package.json. Do not make a lockfile-only commit.
|
|
356
390
|
Do not add Co-authored-by or Made-with trailers.
|
|
357
|
-
${type ? `Prefer type "${type}".\n` : ''}
|
|
358
|
-
Files:
|
|
391
|
+
${type ? `Prefer type "${type}".\n` : ''}Files:
|
|
359
392
|
${files
|
|
360
393
|
.map((file) => {
|
|
361
394
|
const status =
|
|
362
395
|
statusByFile.get(file) || (isUntracked(cwd, file) ? '??' : ' M')
|
|
363
|
-
|
|
396
|
+
const lockNote = isLockfile(file) ? ', lockfile' : ''
|
|
397
|
+
return `- ${file} (${status}${lockNote})`
|
|
364
398
|
})
|
|
365
399
|
.join('\n')}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
diff
|
|
400
|
+
${
|
|
401
|
+
lockFiles.length
|
|
402
|
+
? `Lockfiles (no diff, keep with package.json):\n${lockFiles
|
|
403
|
+
.map((file) => `- ${file}`)
|
|
404
|
+
.join('\n')}\n`
|
|
405
|
+
: ''
|
|
406
|
+
}diff:
|
|
369
407
|
${diff || '(none)'}
|
|
370
408
|
`
|
|
371
409
|
}
|
|
372
410
|
|
|
373
|
-
function
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
)
|
|
411
|
+
function buildSourceDiff(cwd, files) {
|
|
412
|
+
const tracked = []
|
|
413
|
+
const untracked = []
|
|
414
|
+
for (const file of files) {
|
|
415
|
+
if (isUntracked(cwd, file)) untracked.push(file)
|
|
416
|
+
else tracked.push(file)
|
|
380
417
|
}
|
|
381
418
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
? `new file ${file}:\n${untrackedDiff}`
|
|
387
|
-
: `new file ${file}: (empty)`
|
|
419
|
+
const parts = []
|
|
420
|
+
if (tracked.length) {
|
|
421
|
+
const text = out(cwd, ['diff', 'HEAD', '--', ...tracked])
|
|
422
|
+
if (text) parts.push(text)
|
|
388
423
|
}
|
|
424
|
+
for (const file of untracked) {
|
|
425
|
+
const piece = buildUntrackedDiff(cwd, file)
|
|
426
|
+
if (piece) parts.push(piece)
|
|
427
|
+
}
|
|
428
|
+
return parts.join('\n\n')
|
|
429
|
+
}
|
|
389
430
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
431
|
+
function buildUntrackedDiff(cwd, file) {
|
|
432
|
+
const filePath = path.join(cwd, file)
|
|
433
|
+
if (!fs.existsSync(filePath)) return ''
|
|
434
|
+
|
|
435
|
+
const nullPath = process.platform === 'win32' ? 'NUL' : '/dev/null'
|
|
436
|
+
const untrackedDiff = out(cwd, ['diff', '--no-index', nullPath, file])
|
|
437
|
+
return untrackedDiff
|
|
438
|
+
? `new file ${file}:\n${untrackedDiff}`
|
|
439
|
+
: `new file ${file}: (empty)`
|
|
396
440
|
}
|
|
397
441
|
|
|
398
442
|
function buildAgentProgressSteps(fileCount) {
|
|
@@ -604,6 +648,104 @@ function parsePlan(raw, allowedFiles) {
|
|
|
604
648
|
return plans
|
|
605
649
|
}
|
|
606
650
|
|
|
651
|
+
function tryFastPlan(files, cwd, scopes, types) {
|
|
652
|
+
const sourceFiles = files.filter((file) => !isLockfile(file))
|
|
653
|
+
if (sourceFiles.length !== 1) return null
|
|
654
|
+
if (path.basename(sourceFiles[0]) !== 'package.json') return null
|
|
655
|
+
|
|
656
|
+
const message = inferDepsMessage(cwd, sourceFiles[0], scopes, types)
|
|
657
|
+
if (!message) return null
|
|
658
|
+
|
|
659
|
+
return [
|
|
660
|
+
{
|
|
661
|
+
message: message,
|
|
662
|
+
files: [...sourceFiles, ...files.filter(isLockfile)],
|
|
663
|
+
},
|
|
664
|
+
]
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function inferDepsMessage(cwd, file, scopes, types) {
|
|
668
|
+
const type = types.includes('deps')
|
|
669
|
+
? 'deps'
|
|
670
|
+
: types.includes('chore')
|
|
671
|
+
? 'chore'
|
|
672
|
+
: ''
|
|
673
|
+
if (!type) return ''
|
|
674
|
+
|
|
675
|
+
const names = changedDependencyNames(cwd, file)
|
|
676
|
+
if (!names?.length) return ''
|
|
677
|
+
|
|
678
|
+
const scope = pickScope(scopes, ['packages', 'deps', 'other'])
|
|
679
|
+
const subject =
|
|
680
|
+
names.length === 1
|
|
681
|
+
? `bump ${names[0]}`
|
|
682
|
+
: names.length <= 3
|
|
683
|
+
? `bump ${names.join(', ')}`
|
|
684
|
+
: 'bump dependencies'
|
|
685
|
+
return `${type}(${scope}): ${subject}`
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function changedDependencyNames(cwd, file) {
|
|
689
|
+
const current = readJsonFile(path.join(cwd, file))
|
|
690
|
+
if (!current) return null
|
|
691
|
+
|
|
692
|
+
let previous = {}
|
|
693
|
+
const shown = spawnSync('git', ['show', `HEAD:${file}`], {
|
|
694
|
+
cwd: cwd,
|
|
695
|
+
encoding: 'utf8',
|
|
696
|
+
})
|
|
697
|
+
if (!shown.status && shown.stdout) {
|
|
698
|
+
try {
|
|
699
|
+
previous = JSON.parse(shown.stdout)
|
|
700
|
+
} catch {
|
|
701
|
+
return null
|
|
702
|
+
}
|
|
703
|
+
} else if (isUntracked(cwd, file)) {
|
|
704
|
+
return null
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const sections = [
|
|
708
|
+
'dependencies',
|
|
709
|
+
'devDependencies',
|
|
710
|
+
'optionalDependencies',
|
|
711
|
+
'peerDependencies',
|
|
712
|
+
]
|
|
713
|
+
const names = []
|
|
714
|
+
for (const key of sections) {
|
|
715
|
+
const before = previous[key] || {}
|
|
716
|
+
const after = current[key] || {}
|
|
717
|
+
for (const name of new Set([
|
|
718
|
+
...Object.keys(before),
|
|
719
|
+
...Object.keys(after),
|
|
720
|
+
])) {
|
|
721
|
+
if (before[name] !== after[name]) names.push(name)
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const keys = new Set([...Object.keys(previous), ...Object.keys(current)])
|
|
726
|
+
for (const key of keys) {
|
|
727
|
+
if (sections.includes(key)) continue
|
|
728
|
+
if (JSON.stringify(previous[key]) !== JSON.stringify(current[key])) {
|
|
729
|
+
return null
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return names.length ? names : null
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function readJsonFile(filePath) {
|
|
737
|
+
try {
|
|
738
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
739
|
+
} catch {
|
|
740
|
+
return null
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function pickScope(scopes, preferred) {
|
|
745
|
+
const list = buildScopeEnum(scopes)
|
|
746
|
+
return preferred.find((scope) => list.includes(scope)) || list[0] || 'other'
|
|
747
|
+
}
|
|
748
|
+
|
|
607
749
|
function assignOmittedFiles(plans, allowedFiles) {
|
|
608
750
|
const assigned = new Set(plans.flatMap((plan) => plan.files))
|
|
609
751
|
const missing = allowedFiles.filter((file) => !assigned.has(file))
|
|
@@ -619,6 +761,23 @@ function assignOmittedFiles(plans, allowedFiles) {
|
|
|
619
761
|
return plans
|
|
620
762
|
}
|
|
621
763
|
|
|
764
|
+
function attachLockfiles(plans, allowedFiles) {
|
|
765
|
+
const locks = allowedFiles.filter((file) => isLockfile(file))
|
|
766
|
+
if (!locks.length) return plans
|
|
767
|
+
|
|
768
|
+
for (const lock of locks) {
|
|
769
|
+
for (const plan of plans) {
|
|
770
|
+
plan.files = plan.files.filter((file) => file !== lock)
|
|
771
|
+
}
|
|
772
|
+
const manifest = siblingManifest(lock)
|
|
773
|
+
const withManifest = plans.find((plan) => plan.files.includes(manifest))
|
|
774
|
+
const target = withManifest || findBestCommitForFile(plans, lock)
|
|
775
|
+
target.files.push(lock)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return plans.filter((plan) => plan.files.length)
|
|
779
|
+
}
|
|
780
|
+
|
|
622
781
|
function findBestCommitForFile(plans, file) {
|
|
623
782
|
const fileDir = path.dirname(file)
|
|
624
783
|
|
|
@@ -741,6 +900,11 @@ function sleepSync(ms) {
|
|
|
741
900
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
|
|
742
901
|
}
|
|
743
902
|
|
|
903
|
+
function formatElapsed(startedAt) {
|
|
904
|
+
const elapsed = Math.max(0, Math.floor((Date.now() - startedAt) / 1000))
|
|
905
|
+
return `${elapsed}s`
|
|
906
|
+
}
|
|
907
|
+
|
|
744
908
|
function indexLockPath(cwd) {
|
|
745
909
|
const result = spawnSync('git', ['rev-parse', '--git-path', 'index.lock'], {
|
|
746
910
|
cwd: cwd,
|
|
@@ -803,6 +967,8 @@ function git(cwd, args, option = {}) {
|
|
|
803
967
|
stdio: option.inherit ? ['ignore', 'inherit', 'pipe'] : undefined,
|
|
804
968
|
})
|
|
805
969
|
if (!result.status) return result
|
|
970
|
+
// git diff exits 1 when there are differences
|
|
971
|
+
if (result.status === 1 && args[0] === 'diff') return result
|
|
806
972
|
|
|
807
973
|
const stderr = result.stderr || ''
|
|
808
974
|
if (!isIndexLockError(stderr) || attempt === INDEX_LOCK_RETRIES) {
|