@itcase/config 1.6.75 → 1.6.76

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/smart-commit/cli.js +95 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itcase/config",
3
- "version": "1.6.75",
3
+ "version": "1.6.76",
4
4
  "author": "ITCase",
5
5
  "description": "ITCase Config",
6
6
  "engines": {
@@ -24,6 +24,9 @@ const MAX_DIFF = 80_000
24
24
  const AGENT_TIMEOUT_MS = 120_000
25
25
  const AGENT_STEP_MS = 2_500
26
26
  const BANANA_FRAME_MS = 100
27
+ const INDEX_LOCK_RETRIES = 8
28
+ const INDEX_LOCK_WAIT_MS = 250
29
+ const STALE_LOCK_MS = 1500
27
30
  const AGENT_WAIT_STEPS = [
28
31
  'Asking Cursor Agent',
29
32
  'Still thinking',
@@ -115,6 +118,8 @@ async function main(argv) {
115
118
  )
116
119
  }
117
120
 
121
+ ensureGitIndexReady(cwd)
122
+
118
123
  let created = 0
119
124
  for (const plan of activePlans) {
120
125
  const files = plan.files.filter((file) => fileHasChanges(cwd, file))
@@ -135,12 +140,13 @@ async function main(argv) {
135
140
  )
136
141
  }
137
142
 
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}`)
143
+ try {
144
+ git(cwd, ['commit', '-m', plan.message], { inherit: true })
145
+ } catch (error) {
146
+ throw new Error(`git commit failed: ${plan.message}\n${error.message}`, {
147
+ cause: error,
148
+ })
149
+ }
144
150
  created++
145
151
  }
146
152
  process.stdout.write(`Created ${created} commit(s).\n`)
@@ -593,10 +599,89 @@ async function confirm(count) {
593
599
  }
594
600
  }
595
601
 
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
602
+ function isIndexLockError(stderr) {
603
+ return /index\.lock/.test(stderr || '')
604
+ }
605
+
606
+ function sleepSync(ms) {
607
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
608
+ }
609
+
610
+ function indexLockPath(cwd) {
611
+ const result = spawnSync('git', ['rev-parse', '--git-path', 'index.lock'], {
612
+ cwd: cwd,
613
+ encoding: 'utf8',
614
+ })
615
+ const gitPath = (result.stdout || '').trim()
616
+ if (!gitPath) return path.join(cwd, '.git', 'index.lock')
617
+ return path.resolve(cwd, gitPath)
618
+ }
619
+
620
+ function lockHolders(lockPath) {
621
+ const result = spawnSync('lsof', ['-t', lockPath], { encoding: 'utf8' })
622
+ if (result.error || result.status === 127) return null
623
+ return (result.stdout || '').trim().split('\n').filter(Boolean)
624
+ }
625
+
626
+ function clearStaleIndexLock(cwd) {
627
+ const lockPath = indexLockPath(cwd)
628
+ let stat
629
+ try {
630
+ stat = fs.statSync(lockPath)
631
+ } catch {
632
+ return false
633
+ }
634
+ if (Date.now() - stat.mtimeMs < STALE_LOCK_MS) return false
635
+ const holders = lockHolders(lockPath)
636
+ if (holders && holders.length) return false
637
+ try {
638
+ fs.unlinkSync(lockPath)
639
+ return true
640
+ } catch {
641
+ return false
642
+ }
643
+ }
644
+
645
+ function ensureGitIndexReady(cwd) {
646
+ for (let attempt = 1; attempt <= INDEX_LOCK_RETRIES; attempt++) {
647
+ const lockPath = indexLockPath(cwd)
648
+ if (!fs.existsSync(lockPath)) return
649
+ if (attempt === 1) {
650
+ process.stderr.write('Waiting for git index.lock to clear…\n')
651
+ }
652
+ clearStaleIndexLock(cwd)
653
+ if (!fs.existsSync(lockPath)) return
654
+ sleepSync(INDEX_LOCK_WAIT_MS * attempt)
655
+ }
656
+ const lockPath = indexLockPath(cwd)
657
+ if (!fs.existsSync(lockPath)) return
658
+ throw new Error(
659
+ `git index.lock still exists: ${lockPath}\n` +
660
+ 'Another git process may be running. If it crashed, remove the file and retry.',
661
+ )
662
+ }
663
+
664
+ function git(cwd, args, option = {}) {
665
+ for (let attempt = 1; attempt <= INDEX_LOCK_RETRIES; attempt++) {
666
+ const result = spawnSync('git', args, {
667
+ cwd: cwd,
668
+ encoding: 'utf8',
669
+ stdio: option.inherit ? ['ignore', 'inherit', 'pipe'] : undefined,
670
+ })
671
+ if (!result.status) return result
672
+
673
+ const stderr = result.stderr || ''
674
+ if (!isIndexLockError(stderr) || attempt === INDEX_LOCK_RETRIES) {
675
+ if (option.inherit && stderr) process.stderr.write(stderr)
676
+ throw new Error(stderr.trim() || `git ${args.join(' ')}`)
677
+ }
678
+
679
+ if (attempt === 1) {
680
+ process.stderr.write('Waiting for git index.lock to clear…\n')
681
+ }
682
+ clearStaleIndexLock(cwd)
683
+ sleepSync(INDEX_LOCK_WAIT_MS * attempt)
684
+ }
600
685
  }
601
686
 
602
687
  function out(cwd, args) {