@itcase/config 1.6.75 → 1.6.77

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 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]`.
90
+ `smart-commit` — Cursor Agent (`ask`) планирует коммиты по `SCOPES`, печатает сообщения, спрашивает `[y/N]`. Перед коммитом ждёт и снимает устаревший `.git/index.lock`, если его никто не держит.
91
91
 
92
92
  ```text
93
93
  [yellow pixel banana animation]
@@ -146,7 +146,7 @@ CLI ставится в `~/.local/bin`. Логин в Cursor IDE этот CLI н
146
146
 
147
147
  Обновление CLI: `agent update`.
148
148
 
149
- Пока Agent думает, в терминале крутится pixel-анимация dancing banana + статус `(Ns)` на одной области экрана (`log-update`). Для полного лога: `npm run smart-commit -- --verbose --dry-run`.
149
+ Пока Agent думает, в терминале крутится pixel-анимация (случайно nyan cat или dancing banana) + статус `(Ns)` на одной области экрана (`log-update`). Принудительно: `--cat` / `--banana`. Для полного лога: `npm run smart-commit -- --verbose --dry-run`.
150
150
 
151
151
  > **Важно:** в npm есть одноимённый пакет `smart-commit`. Используйте `npm run smart-commit` из проекта с `@itcase/config` — bin берётся из `node_modules`. `npx smart-commit` без локальной установки может подтянуть чужой пакет; CLI перед запуском проверяет, что выполняется именно `@itcase/config`.
152
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itcase/config",
3
- "version": "1.6.75",
3
+ "version": "1.6.77",
4
4
  "author": "ITCase",
5
5
  "description": "ITCase Config",
6
6
  "engines": {
@@ -67,8 +67,8 @@
67
67
  "chokidar-cli": "^3.0.0",
68
68
  "commitizen": "^4.3.2",
69
69
  "conventional-changelog-conventionalcommits": "9.3.1",
70
- "cssnano": "^9.0.1",
71
- "cssnano-preset-default": "^9.0.1",
70
+ "cssnano": "^9.0.2",
71
+ "cssnano-preset-default": "^9.0.2",
72
72
  "inquirer": "^14.2.0",
73
73
  "log-update": "^8.0.0",
74
74
  "postcss": "^8.5.26",
@@ -79,7 +79,7 @@
79
79
  "postcss-combine-duplicated-selectors": "^10.0.3",
80
80
  "postcss-convert-unit": "^0.1.3",
81
81
  "postcss-dark-theme-class": "^2.0.0",
82
- "postcss-discard-duplicates": "^9.0.0",
82
+ "postcss-discard-duplicates": "^9.0.1",
83
83
  "postcss-each": "^1.1.0",
84
84
  "postcss-easings": "^4.0.0",
85
85
  "postcss-extend-rule": "^4.0.0",
@@ -16,6 +16,7 @@ import {
16
16
  promptSubjectRules,
17
17
  } from '../commitlint/commitlintCommon.js'
18
18
  import { getBananaFrame } from './dancing-banana.js'
19
+ import { getNyanFrame } from './nyan-cat.js'
19
20
 
20
21
  const PACKAGE_NAME = '@itcase/config'
21
22
  const CLI_RELATIVE = 'smart-commit/cli.js'
@@ -23,17 +24,24 @@ const CLI_RELATIVE = 'smart-commit/cli.js'
23
24
  const MAX_DIFF = 80_000
24
25
  const AGENT_TIMEOUT_MS = 120_000
25
26
  const AGENT_STEP_MS = 2_500
27
+ const NYAN_FRAME_MS = 90
26
28
  const BANANA_FRAME_MS = 100
29
+ const INDEX_LOCK_RETRIES = 8
30
+ const INDEX_LOCK_WAIT_MS = 250
31
+ const STALE_LOCK_MS = 1500
27
32
  const AGENT_WAIT_STEPS = [
28
33
  'Asking Cursor Agent',
29
34
  'Still thinking',
30
35
  'Waiting for response',
31
36
  ]
32
37
  const HELP = `smart-commit [--dry-run] [--yes|-y] [--type t] [--model id] [--verbose]
38
+ [--cat|--nyan] [--banana]
33
39
 
34
40
  Cursor Agent → commit messages by SCOPES → [y/N] → git commit
35
41
  Run via: npm exec smart-commit (from project with @itcase/config)
36
42
  Needs: agent login (or CURSOR_API_KEY) + commitlint.config.* with SCOPES
43
+
44
+ Progress sprite is random (nyan cat or banana). Force with --cat or --banana.
37
45
  `
38
46
  const AGENT_MISSING = `Cursor Agent CLI not found — smart-commit cannot run.
39
47
 
@@ -71,7 +79,11 @@ async function main(argv) {
71
79
 
72
80
  const prompt = buildPrompt(scopes, types, files, cwd, option.type)
73
81
  const raw = await withAgentProgress(
74
- { fileCount: files.length, verbose: option.verbose },
82
+ {
83
+ fileCount: files.length,
84
+ sprite: pickSprite(option.sprite),
85
+ verbose: option.verbose,
86
+ },
75
87
  (progress) =>
76
88
  askAgent(
77
89
  cwd,
@@ -115,6 +127,8 @@ async function main(argv) {
115
127
  )
116
128
  }
117
129
 
130
+ ensureGitIndexReady(cwd)
131
+
118
132
  let created = 0
119
133
  for (const plan of activePlans) {
120
134
  const files = plan.files.filter((file) => fileHasChanges(cwd, file))
@@ -135,12 +149,13 @@ async function main(argv) {
135
149
  )
136
150
  }
137
151
 
138
- const commit = spawnSync('git', ['commit', '-m', plan.message], {
139
- cwd: cwd,
140
- encoding: 'utf8',
141
- stdio: ['ignore', 'inherit', 'inherit'],
142
- })
143
- if (commit.status) throw new Error(`git commit failed: ${plan.message}`)
152
+ try {
153
+ git(cwd, ['commit', '-m', plan.message], { inherit: true })
154
+ } catch (error) {
155
+ throw new Error(`git commit failed: ${plan.message}\n${error.message}`, {
156
+ cause: error,
157
+ })
158
+ }
144
159
  created++
145
160
  }
146
161
  process.stdout.write(`Created ${created} commit(s).\n`)
@@ -151,6 +166,7 @@ function parseArgs(argv) {
151
166
  type: '',
152
167
  dryRun: false,
153
168
  model: '',
169
+ sprite: '',
154
170
  staged: false,
155
171
  verbose: false,
156
172
  yes: false,
@@ -161,6 +177,8 @@ function parseArgs(argv) {
161
177
  else if (arg === '--yes' || arg === '-y') option.yes = true
162
178
  else if (arg === '--verbose') option.verbose = true
163
179
  else if (arg === '--staged') option.staged = true
180
+ else if (arg === '--cat' || arg === '--nyan') option.sprite = 'cat'
181
+ else if (arg === '--banana') option.sprite = 'banana'
164
182
  else if (arg === '--type') option.type = argv[++i] || ''
165
183
  else if (arg === '--model') option.model = argv[++i] || ''
166
184
  else if (arg.startsWith('--type=')) option.type = arg.slice(7)
@@ -170,6 +188,11 @@ function parseArgs(argv) {
170
188
  return option
171
189
  }
172
190
 
191
+ function pickSprite(forced) {
192
+ if (forced === 'cat' || forced === 'banana') return forced
193
+ return Math.random() < 0.5 ? 'cat' : 'banana'
194
+ }
195
+
173
196
  function assertItcaseConfigPackage(cwd) {
174
197
  const scriptPath = fileURLToPath(import.meta.url)
175
198
 
@@ -375,13 +398,21 @@ async function withAgentProgress(context, callback) {
375
398
  return callback({ onAgentActivity })
376
399
  }
377
400
 
401
+ const columns = process.stderr.columns || process.stdout.columns || 80
402
+ const rows = process.stderr.rows || process.stdout.rows || 24
403
+ const getFrame =
404
+ context.sprite === 'cat'
405
+ ? (index) => getNyanFrame(index, columns)
406
+ : (index) => getBananaFrame(index, columns, rows)
407
+ const frameMs = context.sprite === 'cat' ? NYAN_FRAME_MS : BANANA_FRAME_MS
408
+ process.stderr.write('\x1b[?7l')
409
+
378
410
  const render = () => {
379
- logUpdateStderr(`${getBananaFrame(frameIndex)}\n${formatText()}`)
411
+ logUpdateStderr(`${getFrame(frameIndex)}\n${formatText()}`)
380
412
  frameIndex++
381
413
  }
382
414
 
383
- render()
384
- const frameTimer = setInterval(render, BANANA_FRAME_MS)
415
+ const frameTimer = setInterval(render, frameMs)
385
416
  const stepTimer = setInterval(() => {
386
417
  if (phase === 'early') {
387
418
  if (earlyStepIndex < earlySteps.length - 1) {
@@ -395,11 +426,13 @@ async function withAgentProgress(context, callback) {
395
426
  }, AGENT_STEP_MS)
396
427
 
397
428
  try {
429
+ render()
398
430
  return await callback({ onAgentActivity })
399
431
  } finally {
400
432
  clearInterval(frameTimer)
401
433
  clearInterval(stepTimer)
402
434
  logUpdateStderr.clear()
435
+ process.stderr.write('\x1b[?7h')
403
436
  }
404
437
  }
405
438
 
@@ -593,10 +626,89 @@ async function confirm(count) {
593
626
  }
594
627
  }
595
628
 
596
- function git(cwd, args) {
597
- const result = spawnSync('git', args, { cwd: cwd, encoding: 'utf8' })
598
- if (result.status) throw new Error(result.stderr || `git ${args.join(' ')}`)
599
- return result
629
+ function isIndexLockError(stderr) {
630
+ return /index\.lock/.test(stderr || '')
631
+ }
632
+
633
+ function sleepSync(ms) {
634
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
635
+ }
636
+
637
+ function indexLockPath(cwd) {
638
+ const result = spawnSync('git', ['rev-parse', '--git-path', 'index.lock'], {
639
+ cwd: cwd,
640
+ encoding: 'utf8',
641
+ })
642
+ const gitPath = (result.stdout || '').trim()
643
+ if (!gitPath) return path.join(cwd, '.git', 'index.lock')
644
+ return path.resolve(cwd, gitPath)
645
+ }
646
+
647
+ function lockHolders(lockPath) {
648
+ const result = spawnSync('lsof', ['-t', lockPath], { encoding: 'utf8' })
649
+ if (result.error || result.status === 127) return null
650
+ return (result.stdout || '').trim().split('\n').filter(Boolean)
651
+ }
652
+
653
+ function clearStaleIndexLock(cwd) {
654
+ const lockPath = indexLockPath(cwd)
655
+ let stat
656
+ try {
657
+ stat = fs.statSync(lockPath)
658
+ } catch {
659
+ return false
660
+ }
661
+ if (Date.now() - stat.mtimeMs < STALE_LOCK_MS) return false
662
+ const holders = lockHolders(lockPath)
663
+ if (holders && holders.length) return false
664
+ try {
665
+ fs.unlinkSync(lockPath)
666
+ return true
667
+ } catch {
668
+ return false
669
+ }
670
+ }
671
+
672
+ function ensureGitIndexReady(cwd) {
673
+ for (let attempt = 1; attempt <= INDEX_LOCK_RETRIES; attempt++) {
674
+ const lockPath = indexLockPath(cwd)
675
+ if (!fs.existsSync(lockPath)) return
676
+ if (attempt === 1) {
677
+ process.stderr.write('Waiting for git index.lock to clear…\n')
678
+ }
679
+ clearStaleIndexLock(cwd)
680
+ if (!fs.existsSync(lockPath)) return
681
+ sleepSync(INDEX_LOCK_WAIT_MS * attempt)
682
+ }
683
+ const lockPath = indexLockPath(cwd)
684
+ if (!fs.existsSync(lockPath)) return
685
+ throw new Error(
686
+ `git index.lock still exists: ${lockPath}\n` +
687
+ 'Another git process may be running. If it crashed, remove the file and retry.',
688
+ )
689
+ }
690
+
691
+ function git(cwd, args, option = {}) {
692
+ for (let attempt = 1; attempt <= INDEX_LOCK_RETRIES; attempt++) {
693
+ const result = spawnSync('git', args, {
694
+ cwd: cwd,
695
+ encoding: 'utf8',
696
+ stdio: option.inherit ? ['ignore', 'inherit', 'pipe'] : undefined,
697
+ })
698
+ if (!result.status) return result
699
+
700
+ const stderr = result.stderr || ''
701
+ if (!isIndexLockError(stderr) || attempt === INDEX_LOCK_RETRIES) {
702
+ if (option.inherit && stderr) process.stderr.write(stderr)
703
+ throw new Error(stderr.trim() || `git ${args.join(' ')}`)
704
+ }
705
+
706
+ if (attempt === 1) {
707
+ process.stderr.write('Waiting for git index.lock to clear…\n')
708
+ }
709
+ clearStaleIndexLock(cwd)
710
+ sleepSync(INDEX_LOCK_WAIT_MS * attempt)
711
+ }
600
712
  }
601
713
 
602
714
  function out(cwd, args) {