@itcase/config 1.6.78 → 1.6.79
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 +195 -47
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')
|
|
@@ -332,6 +344,15 @@ function listFiles(cwd, option) {
|
|
|
332
344
|
]
|
|
333
345
|
}
|
|
334
346
|
|
|
347
|
+
function isLockfile(file) {
|
|
348
|
+
return LOCKFILE_NAMES.has(path.basename(file))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function siblingManifest(file) {
|
|
352
|
+
const directory = path.posix.dirname(file)
|
|
353
|
+
return directory === '.' ? 'package.json' : `${directory}/package.json`
|
|
354
|
+
}
|
|
355
|
+
|
|
335
356
|
function buildPrompt(scopes, types, files, cwd, type) {
|
|
336
357
|
const statusByFile = new Map(
|
|
337
358
|
lines(cwd, ['status', '--short', '--porcelain', '--', ...files]).map(
|
|
@@ -339,10 +360,9 @@ function buildPrompt(scopes, types, files, cwd, type) {
|
|
|
339
360
|
),
|
|
340
361
|
)
|
|
341
362
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.join('\n\n')
|
|
363
|
+
const sourceFiles = files.filter((file) => !isLockfile(file))
|
|
364
|
+
const lockFiles = files.filter((file) => isLockfile(file))
|
|
365
|
+
let diff = buildSourceDiff(cwd, sourceFiles)
|
|
346
366
|
if (diff.length > MAX_DIFF) diff = `${diff.slice(0, MAX_DIFF)}\n…truncated`
|
|
347
367
|
|
|
348
368
|
return `Plan git commits. Do not edit files or run git commit.
|
|
@@ -353,46 +373,57 @@ Subject rules:
|
|
|
353
373
|
${promptSubjectRules.map((rule) => `- ${rule}`).join('\n')}
|
|
354
374
|
Prefer one commit when changes belong to the same feature.
|
|
355
375
|
Split by scope. Each listed file in exactly one commit — including new/untracked files (??).
|
|
376
|
+
Put each lockfile in the same commit as its sibling package.json. Do not make a lockfile-only commit.
|
|
356
377
|
Do not add Co-authored-by or Made-with trailers.
|
|
357
|
-
${type ? `Prefer type "${type}".\n` : ''}
|
|
358
|
-
Files:
|
|
378
|
+
${type ? `Prefer type "${type}".\n` : ''}Files:
|
|
359
379
|
${files
|
|
360
380
|
.map((file) => {
|
|
361
381
|
const status =
|
|
362
382
|
statusByFile.get(file) || (isUntracked(cwd, file) ? '??' : ' M')
|
|
363
|
-
|
|
383
|
+
const lockNote = isLockfile(file) ? ', lockfile' : ''
|
|
384
|
+
return `- ${file} (${status}${lockNote})`
|
|
364
385
|
})
|
|
365
386
|
.join('\n')}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
diff
|
|
387
|
+
${
|
|
388
|
+
lockFiles.length
|
|
389
|
+
? `Lockfiles (no diff, keep with package.json):\n${lockFiles
|
|
390
|
+
.map((file) => `- ${file}`)
|
|
391
|
+
.join('\n')}\n`
|
|
392
|
+
: ''
|
|
393
|
+
}diff:
|
|
369
394
|
${diff || '(none)'}
|
|
370
395
|
`
|
|
371
396
|
}
|
|
372
397
|
|
|
373
|
-
function
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
)
|
|
398
|
+
function buildSourceDiff(cwd, files) {
|
|
399
|
+
const tracked = []
|
|
400
|
+
const untracked = []
|
|
401
|
+
for (const file of files) {
|
|
402
|
+
if (isUntracked(cwd, file)) untracked.push(file)
|
|
403
|
+
else tracked.push(file)
|
|
380
404
|
}
|
|
381
405
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
406
|
+
const parts = []
|
|
407
|
+
if (tracked.length) {
|
|
408
|
+
const text = out(cwd, ['diff', 'HEAD', '--', ...tracked])
|
|
409
|
+
if (text) parts.push(text)
|
|
410
|
+
}
|
|
411
|
+
for (const file of untracked) {
|
|
412
|
+
const piece = buildUntrackedDiff(cwd, file)
|
|
413
|
+
if (piece) parts.push(piece)
|
|
388
414
|
}
|
|
415
|
+
return parts.join('\n\n')
|
|
416
|
+
}
|
|
389
417
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
418
|
+
function buildUntrackedDiff(cwd, file) {
|
|
419
|
+
const filePath = path.join(cwd, file)
|
|
420
|
+
if (!fs.existsSync(filePath)) return ''
|
|
421
|
+
|
|
422
|
+
const nullPath = process.platform === 'win32' ? 'NUL' : '/dev/null'
|
|
423
|
+
const untrackedDiff = out(cwd, ['diff', '--no-index', nullPath, file])
|
|
424
|
+
return untrackedDiff
|
|
425
|
+
? `new file ${file}:\n${untrackedDiff}`
|
|
426
|
+
: `new file ${file}: (empty)`
|
|
396
427
|
}
|
|
397
428
|
|
|
398
429
|
function buildAgentProgressSteps(fileCount) {
|
|
@@ -604,6 +635,104 @@ function parsePlan(raw, allowedFiles) {
|
|
|
604
635
|
return plans
|
|
605
636
|
}
|
|
606
637
|
|
|
638
|
+
function tryFastPlan(files, cwd, scopes, types) {
|
|
639
|
+
const sourceFiles = files.filter((file) => !isLockfile(file))
|
|
640
|
+
if (sourceFiles.length !== 1) return null
|
|
641
|
+
if (path.basename(sourceFiles[0]) !== 'package.json') return null
|
|
642
|
+
|
|
643
|
+
const message = inferDepsMessage(cwd, sourceFiles[0], scopes, types)
|
|
644
|
+
if (!message) return null
|
|
645
|
+
|
|
646
|
+
return [
|
|
647
|
+
{
|
|
648
|
+
message: message,
|
|
649
|
+
files: [...sourceFiles, ...files.filter(isLockfile)],
|
|
650
|
+
},
|
|
651
|
+
]
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function inferDepsMessage(cwd, file, scopes, types) {
|
|
655
|
+
const type = types.includes('deps')
|
|
656
|
+
? 'deps'
|
|
657
|
+
: types.includes('chore')
|
|
658
|
+
? 'chore'
|
|
659
|
+
: ''
|
|
660
|
+
if (!type) return ''
|
|
661
|
+
|
|
662
|
+
const names = changedDependencyNames(cwd, file)
|
|
663
|
+
if (!names?.length) return ''
|
|
664
|
+
|
|
665
|
+
const scope = pickScope(scopes, ['packages', 'deps', 'other'])
|
|
666
|
+
const subject =
|
|
667
|
+
names.length === 1
|
|
668
|
+
? `bump ${names[0]}`
|
|
669
|
+
: names.length <= 3
|
|
670
|
+
? `bump ${names.join(', ')}`
|
|
671
|
+
: 'bump dependencies'
|
|
672
|
+
return `${type}(${scope}): ${subject}`
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function changedDependencyNames(cwd, file) {
|
|
676
|
+
const current = readJsonFile(path.join(cwd, file))
|
|
677
|
+
if (!current) return null
|
|
678
|
+
|
|
679
|
+
let previous = {}
|
|
680
|
+
const shown = spawnSync('git', ['show', `HEAD:${file}`], {
|
|
681
|
+
cwd: cwd,
|
|
682
|
+
encoding: 'utf8',
|
|
683
|
+
})
|
|
684
|
+
if (!shown.status && shown.stdout) {
|
|
685
|
+
try {
|
|
686
|
+
previous = JSON.parse(shown.stdout)
|
|
687
|
+
} catch {
|
|
688
|
+
return null
|
|
689
|
+
}
|
|
690
|
+
} else if (isUntracked(cwd, file)) {
|
|
691
|
+
return null
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const sections = [
|
|
695
|
+
'dependencies',
|
|
696
|
+
'devDependencies',
|
|
697
|
+
'optionalDependencies',
|
|
698
|
+
'peerDependencies',
|
|
699
|
+
]
|
|
700
|
+
const names = []
|
|
701
|
+
for (const key of sections) {
|
|
702
|
+
const before = previous[key] || {}
|
|
703
|
+
const after = current[key] || {}
|
|
704
|
+
for (const name of new Set([
|
|
705
|
+
...Object.keys(before),
|
|
706
|
+
...Object.keys(after),
|
|
707
|
+
])) {
|
|
708
|
+
if (before[name] !== after[name]) names.push(name)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const keys = new Set([...Object.keys(previous), ...Object.keys(current)])
|
|
713
|
+
for (const key of keys) {
|
|
714
|
+
if (sections.includes(key)) continue
|
|
715
|
+
if (JSON.stringify(previous[key]) !== JSON.stringify(current[key])) {
|
|
716
|
+
return null
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return names.length ? names : null
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function readJsonFile(filePath) {
|
|
724
|
+
try {
|
|
725
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
726
|
+
} catch {
|
|
727
|
+
return null
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function pickScope(scopes, preferred) {
|
|
732
|
+
const list = buildScopeEnum(scopes)
|
|
733
|
+
return preferred.find((scope) => list.includes(scope)) || list[0] || 'other'
|
|
734
|
+
}
|
|
735
|
+
|
|
607
736
|
function assignOmittedFiles(plans, allowedFiles) {
|
|
608
737
|
const assigned = new Set(plans.flatMap((plan) => plan.files))
|
|
609
738
|
const missing = allowedFiles.filter((file) => !assigned.has(file))
|
|
@@ -619,6 +748,23 @@ function assignOmittedFiles(plans, allowedFiles) {
|
|
|
619
748
|
return plans
|
|
620
749
|
}
|
|
621
750
|
|
|
751
|
+
function attachLockfiles(plans, allowedFiles) {
|
|
752
|
+
const locks = allowedFiles.filter((file) => isLockfile(file))
|
|
753
|
+
if (!locks.length) return plans
|
|
754
|
+
|
|
755
|
+
for (const lock of locks) {
|
|
756
|
+
for (const plan of plans) {
|
|
757
|
+
plan.files = plan.files.filter((file) => file !== lock)
|
|
758
|
+
}
|
|
759
|
+
const manifest = siblingManifest(lock)
|
|
760
|
+
const withManifest = plans.find((plan) => plan.files.includes(manifest))
|
|
761
|
+
const target = withManifest || findBestCommitForFile(plans, lock)
|
|
762
|
+
target.files.push(lock)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return plans.filter((plan) => plan.files.length)
|
|
766
|
+
}
|
|
767
|
+
|
|
622
768
|
function findBestCommitForFile(plans, file) {
|
|
623
769
|
const fileDir = path.dirname(file)
|
|
624
770
|
|
|
@@ -803,6 +949,8 @@ function git(cwd, args, option = {}) {
|
|
|
803
949
|
stdio: option.inherit ? ['ignore', 'inherit', 'pipe'] : undefined,
|
|
804
950
|
})
|
|
805
951
|
if (!result.status) return result
|
|
952
|
+
// git diff exits 1 when there are differences
|
|
953
|
+
if (result.status === 1 && args[0] === 'diff') return result
|
|
806
954
|
|
|
807
955
|
const stderr = result.stderr || ''
|
|
808
956
|
if (!isIndexLockError(stderr) || attempt === INDEX_LOCK_RETRIES) {
|